Package: sanic-attrs
Sanic with attrs towards Swagger 2.0 / OpenAPI supportSupercharge yourSanicapp with:attrsSwaggerNote: This is a fork of Sanic OpenAPI implementation from@channelcat, which I like a lot but it lacks some of the functionality I wanted (and I also went sideways by using a third-party lib (attrs) as default for modeling input / output model classes).Super quick introductionGive your Sanic API an UI and OpenAPI documentation, all for the price of free!InstallationAttention: since this fork came from a necessity of mine, a lot of features I want to implement are still not available, hence the status ofpre-alphato this library! Also,don't try the examples folder, it was not converted (yet)! Shame on me ...pipinstallsanic-attrsAdd OpenAPI and Swagger UI:fromsanic_attrsimportswagger_blueprint,openapi_blueprintapp.blueprint(openapi_blueprint)app.blueprint(swagger_blueprint)You'll now have a Swagger UI at the URL/swagger. Your routes will be automatically categorized by their blueprints. This is the default usage, but more advanced usage can be seen. Keep reading!Note: theswagger_blueprintis awesome but sometimes you don't want it open-wide for whatever reason you have (security, etc), so you can make it available only if running withdebug=True, for example. That's how I actually use it :smile:typingSincesanic-attrsis, of course, based onattrsand the Python target version is 3.5+, most of the typing definitions for your model will be made entirely using Python types, either global ones or from thetypinglibrary. Also,enumsare supported as well! :sparkles:Here's the types supported (so far):intfloatstrbooldatedatetimebytestyping.Anytyping.Collectiontyping.Dicttyping.Iterabletyping.Listtyping.Mappingtyping.Optionaltyping.Sequencetyping.Settyping.UnionA note onlistanddict: Please, usetyping.Listandtyping.Dictfor this.UsageUse simple decorators to document routesfromsanic_attrsimportdoc@app.get("/user/<user_id:int>")@doc.summary("Fetches a user by ID")@doc.produces(SomeOutputModel)asyncdefget_user(request,user_id):...@app.post("/user")@doc.summary("Creates a user")@doc.consumes(SomeInputModel,location="body")asyncdefcreate_user(request):...Model your input/outputYes, in this version youneedto be descriptive :wink:importtypingfromsanic_attrsimportdocclassCar(doc.Model):make:str=doc.field(description="Who made the car")model:str=doc.field(description="Type of car. This will vary by make")year:int=doc.field(description="4-digit year of the car",required=False)classGarage(doc.Model):spaces:int=doc.field(description="How many cars can fit in the garage")cars:typing.List[Car]=doc.field(description="All cars in the garage")@app.get("/garage")@doc.summary("Gets the whole garage")@doc.produces(Garage)asyncdefget_garage(request):returnjson({"spaces":2,"cars":[{"make":"Nissan","model":"370Z"}]})Advanced usageSincedoc.Modelanddoc.fieldare nothing more as syntatic sugar for the@attr.sdecorator andattr.ibfunction, you can express your models using these provided classes and methods or use vanillaattrsin your models. Here's a complex example that shows a mixed model:fromenumimportEnum,IntEnumfromtypingimport(Any,Collection,Dict,Iterable,List,Mapping,Optional,Sequence,Set,Union)importattrfromsanic_attrsimportdocclassPlatformEnum(str,Enum):XBOX1="XBOX1"PLAYSTATION4="PLAYSTATION4"PC="PC"classLanguageEnum(IntEnum):ENGLISH=1JAPANESE=2SPANISH=3GERMAN=4PORTUGUESE=5classSomething(doc.Model):some_name:str=doc.field(description="Something name")@attr.sclassAnotherSomething:another_name:str=attr.ib(metadata={"description":"Another field"})classGame(doc.Model):name:str=doc.field(description="The name of the game")platform:PlatformEnum=doc.field(description="Which platform it runs on")score:float=doc.field(description="The average score of the game")resolution_tested:str=doc.field(description="The resolution which the game was tested")genre:List[str]=doc.field(description="One or more genres this game is part of")genre_extra:Sequence[str]=doc.field(description="One or more genres this game is part of")rating:Dict[str,float]=doc.field(description="Ratings given on each country")rating_outside:Mapping[str,float]=doc.field(description="Ratings given on each country")screenshots:Set[bytes]=doc.field(description="Screenshots of the game")screenshots_extra:Collection[bytes]=doc.field(description="Screenshots of the game")players:Iterable[str]=doc.field(description="Some of the notorious players of this game")review_link:Optional[str]=doc.field(description="The link of the game review (if exists)")junk:Union[str,bytes]=doc.field(description="This should be strange")more_junk:Any=doc.field(description="The more junk field")language:LanguageEnum=doc.field(description="The language of the game")something:List[Something]=doc.field(description="Something to go along the game")another:AnotherSomething=doc.field(description="Another something to go along the game")A note on typing hints ortypeargumentYou may have noticed that in the example above, all variables have been created using typing hints. While this is somewhat interesting, you may also want to use thetypeargument as provided from theattrpackage, andsanic-attrsis absolutely fine with that. So, ourGameclass would rather looks like:classGame(doc.Model):name=doc.field(type=str,description="The name of the game")platform=doc.field(type=PlatformEnum,description="Which platform it runs on")score=doc.field(type=float,description="The average score of the game")resolution_tested=doc.field(type=str,description="The resolution which the game was tested")genre=doc.field(type=List[str],description="One or more genres this game is part of")genre_extra=doc.field(type=Sequence[str],description="One or more genres this game is part of")rating=doc.field(type=Dict[str,float],description="Ratings given on each country")rating_outside=doc.field(type=Mapping[str,float],description="Ratings given on each country")screenshots=doc.field(type=Set[bytes],description="Screenshots of the game")screenshots_extra=doc.field(type=Collection[bytes],description="Screenshots of the game")players=doc.field(type=Iterable[str],description="Some of the notorious players of this game")review_link=doc.field(type=Optional[str],description="The link of the game review (if exists)")junk=doc.field(type=Union[str,bytes],description="This should be strange")more_junk=doc.field(type=Any,description="The more junk field")language=doc.field(type=LanguageEnum,description="The language of the game")something=doc.field(type=List[Something],description="Something to go along the game")another=doc.field(type=AnotherSomething,description="Another something to go along the game")A note on a lot of features ofattrsThere are a lot of features inattrsthat can be handy while declaring a model, such as validators, factories and etc. For this release, some syntatic sugar is planned regarding validators (since most of the rules can be provided todoc.field). Other features, likefactories, are not encourage at this time (or for the lifetime of this project, undecided) while declaring models since there wasn't enough time to actually test them (so far) :confused:On-the-fly input model parsingThere are a few surprises insidesanic-attrs. Let's say you have already declared your model, your endpoint and you still have to take therequest.jsonand load it as your model? That doesn't seems right ... Fortunatelly, a small middleware was written to handle these cases :wink:To enable on-the-fly input model parsing, all you need to do is add ablueprintto your Sanic app and access the object using theinput_objkeyword directly from the request:fromsanic_attrsimportparser_blueprint# ...app.blueprint(parser_blueprint)# ...@app.post("/game",strict_slashes=True)@doc.summary("Inserts the game data into the database")@doc.response("200","Game inserted successfuly",model=SuccessOutput)@doc.response("403","The user couldn't insert game to application",model=ErrorOutput)@doc.consumes(Game,location="body",content_type="application/json")@doc.produces(SuccessOutput)asyncdefinsert_game(request):my_object=request["input_obj"]assertisinstance(my_object,Game)# your logic hereNote: there are no validations to deal with (really) broken data. If an exception occurs while populating your model, you will find that yourinput_objkeyword will beNone, along with another key,input_exc, that will contain the exception given (if any). If you want to further customize this behavior so you won't need to check forNonein every request, you can add your ownmiddlewareafteradding theparser_blueprintto theappinstance, like the following:fromsanic.responseimportjsonfromsanic_attrsimportparser_blueprint# ...app.blueprint(parser_blueprint)# ...@app.middleware("request")asyncdefcheck_if_input_is_none(request):if"input_obj"inrequest:ifrequest["input_obj"]isNone:# error handling herereturnjson({"error":request["input_exc"].args[0]},500)On-the-fly output model serializationTo keep things simple, it is also possible to handle the direct return ofattrsobjects, instead of having to create a dictionary and then serialize or callsanic.responses.json, although this is exactly what's running under the hood:fromsanic_attrsimportresponse# ...@app.get("/game",strict_slashes=True)@doc.summary("Gets the most played game in our database")@doc.response("200","Game data",model=Game)@doc.response("403","The user can't access this endpoint",model=ErrorOutput)@doc.produces(Game)asyncdefget_game(request):game=Game(name="Cities: Skylines",platform="PC",score=9.0,resolution_tested="1920x1080",genre=["Simulators","City Building"],rating={"IGN":8.5,"Gamespot":8.0,"Steam":4.5},players=["Flux","strictoaster"],language=1)returnresponse.model(game)# <--- the game instance, to be further serializedNote: remember to create models that can have all its values serializable to JSON :+1:Configure everything elseapp.config.API_VERSION='1.0.0'app.config.API_TITLE='Car API'app.config.API_DESCRIPTION='Car API'app.config.API_TERMS_OF_SERVICE='Use with caution!'app.config.API_PRODUCES_CONTENT_TYPES=['application/json']app.config.API_CONTACT_EMAIL='channelcat@gmail.com'Types notyetavaiableThese are the types not available fromtypingin the current version (with some notes so I can remember what to do later (if necessary)):AbstractSet- would be like set?AnyStr- this is mostly like Optional[str] or just str?AsyncContextManager- not a variable I thinkAsyncGenerator- not a variable I thinkAsyncIterable- not a variable I thinkAsyncIterator- not a variable I thinkAwaitable- not a variable I thinkBinaryIO- hmmm, I don't know ... Bytes maybe?ByteString- could be like bytes, for openapi is{"type":"string", "format": "byte"}CT_co- I don't even know what this is ...Callable- not a variableCallableMeta- not a variableChainMap- not a variable (?)ClassVar- generic ...Container- genericContextManager- not a variableCoroutine- not a variableCounter- not a variableDefaultDict- perhaps like dict?Deque- like List ?FrozenSet- a "view-only list?Generator- not a variableGeneric- no way - or Any?Hashable- a hashmap?IO- hmmm, from docs: "Generic base class for TextIO and BinaryIO.", so ...ItemsView- what is an Item? it inherits from AbstractSet ... from docs: "A set is a finite, iterable container."Iterator- not a variableKT- genericsKeysView- dict "readonly" ?MappingView- dict "readonly" ?Match- generic (I think)MethodDescriptorType- not a variableMethodWrapperType- not a variableMutableMapping- base class of Mapping, docs: "Abstract base class for generic types."MutableSequence- same as above, but for SequenceMutableSet- same as above, but for SetNamedTuple- what to do here? NamedTuple is just an object with variables that can beanythingI guess ...NamedTupleMeta- baseclass of NamedTupleNewType- not a variable / generic ?NoReturn- not a variablePattern- genericReversible- generic (Iterable)Sized- genericSupportsAbs- not a variableSupportsBytes- not a variableSupportsComplex- not a variableSupportsFloat- not a variableSupportsInt- not a variableSupportsRound- not a variableT- genericTYPE_CHECKING- ???T_co- ???T_contra- ???Text- returns a str object if created, so I'll stick with str or map it too?TextIO- buffer, like bytes ... map it?Tuple- well ... Tuple like lists or Tuple like Tuple[int, str, float] ?TupleMeta- baseclass of TupleType- genericsTypeVar- genericsTypingMeta- genericsIf there's anything missing or required, please fill in a issue or contribute with a PR. PR's are most welcome :smiley:TODOProperty deal withrequiredfields (in OpenAPIobjectschema)Use type hinting to document the return of a function (as output schema / model)Proper testingIncrease use casesFind out if I can get the request model without calling the routerDocumentationLicenseMIT, the same assanic-openapi.