For this to begin, I am not really motivated in beginning with a full fledged MVC (Model View Controller) à la django because there is a lot of boilerplates and actions to do before a result. But, it has a lot of feature I want, including authentication, authorization and handling security.
For prototypes we normally flavours lightweight framework (à la flask), and CRUD.
CRUD approach is a factorisation of all framework in a single dynamic form that adapts itself to the model to generate HTML forms to input data, tabulate, REST endpoints and search them from the python class declaration and generate the database model. One language to rule them all : PYTHON. You can easily generate even the javascript to handle autocompletion on the generated view from python with enough talent.
But before using a CRUD framework, we need a cruder one, ugly, disgusting but useful for a human before building the REST APIs, writing the class in python, the HTML form, and the controlers.
I call this the crudest CRUD of them all.
Think hard at what you want when prototyping ...
- to write no CONTROLLERS ; flask documentation has a very verbose approach to exposing routes and writing them, writing controller for embasing and searching databases is boring
- to write the fewer HTML views possible, one and only onle would be great ;
- to avoid having to fiddle the many files reflecting separation of concerns : the lesser python files and class you touch the better;
- to avoid having to write SQL nor use an ORM (at least a verbose declarative one) ;
- show me your code and you can mesmerize and even fool me, however show me your data structure and I'll know everthing I have to know about your application : data structure should be under your nose in a readable fashion in the code;/
- to have AT LEAST one end point for inserting and searching so that curl can be used to begin automation and testing, preferably in a factorisable fashion;
- only one point of failure is accepted
Once we set these few condition we see whatever we do WE NEED a dynamic http server at the core. Python being the topic here, we are gonna do it in python.
What is the simplest dynamic web server in python ?
The reference implementation of wsgi that is the crudest wsgi server of them all : wsgiref. And you don't need to download it since it's provided in python stdlib.
First thing first, we are gonna had a default view so that we can serve an HTML static page with the list of the minimal HTML we need to interact with data : sets of input and forms.
Here, we stop. And we see that these forms are describing the data model.
Wouldn't it be nice if we could parse the HTML form easily with a tool from the standard library : html.parser and maybe deduce the database model and even more than fields coud add relationship, and well since we are dreaming : what about creating the tables on the fly from the form if they don't exists ?
The encoding of the relationship do require an hijack of convention where when the parser cross a name of the field in the form whatever_id it deduces it is a foreign key to table « whatever », column « id ».
Once this is done, we can parse the html, do some magick to match HTML input types to database types (adapter) and it's almost over. We can even dream of creating the database if it does not exists in a oneliner for sqlite.
We just need to throw away all the frugality of dependencies by the window and spoil our karma of « digital soberty » by adding the almighty sqlalchemy the crudest (but still heavy) ORM when it comes of the field of the introspective features of an ORM to map a database object to a python object in a clear consistent way. With this, just one function is needed in the controller to switch from embasing (POST method) and searching (GET).
Well, if the DOM is passed in the request. So of course I see the critics here :
- we can't pass the DOM in the request because the HTML form ignores the DOM
- You are not scared of error 415 (request too large) in the get method if you pass the DOM ?
Since we are human we would also like the form to be readable when served, because, well, human don't read the source and can't see the name attributes of the input. A tad of improving the raw html would be nice. It would also give consistency. It will also diminishes the required size of the formular to send. Here, javascript again is the right anwser. Fine, we serve the static page in the top of the controller. Let's use jquery to make it terse enough. Oh, if we have Javascript, wouldn't il be able to clone the part of the invented model tag inside every form so now we can pass the relevant part of the DOM to the controller ?
I think we have everything to write the crudest CRUD server of them all :D
Happy code reading :
import multipart from wsgiref.simple_server import make_server from json import dumps from sqlalchemy import * from html.parser import HTMLParser from base64 import b64encode from sqlalchemy.ext.automap import automap_base from sqlalchemy.orm import Session from dateutil import parser from sqlalchemy_utils import database_exists, create_database from urllib.parse import parse_qsl, urlparse engine = create_engine("sqlite:///this.db") if not database_exists(engine.url): create_database(engine.url) tables = dict() class HTMLtoData(HTMLParser): def __init__(self): global engine, tables self.cols = [] self.table = "" self.tables= [] self.engine= engine self.meta = MetaData() super().__init__() def handle_starttag(self, tag, attrs): attrs = dict(attrs) simple_mapping = dict( email = UnicodeText, url = UnicodeText, phone = UnicodeText, text = UnicodeText, date = Date, time = Time, datetime = DateTime, file = Text ) if tag == "input": if attrs.get("name") == "id": self.cols += [ Column('id', Integer, primary_key = True), ] return try: if attrs.get("name").endswith("_id"): table,_=attrs.get("name").split("_") self.cols += [ Column(attrs["name"], Integer, ForeignKey(table + ".id")) ] return except Exception as e: print(e) if attrs.get("type") in simple_mapping.keys(): self.cols += [ Column(attrs["name"], simple_mapping[attrs["type"]]), ] if attrs["type"] == "number": if attrs["step"] == "any": self.cols+= [ Columns(attrs["name"], Float), ] else: self.cols+= [ Column(attrs["name"], Integer), ] if tag== "form": self.table = urlparse(attrs["action"]).path[1:] def handle_endtag(self, tag): if tag=="form": self.tables += [ Table(self.table, self.meta, *self.cols), ] tables[self.table] = self.tables[-1] self.table = "" self.cols = [] with engine.connect() as cnx: self.meta.create_all(engine) cnx.commit() html = """ <!doctype html> <html> <head> <style> * { font-family:"Sans Serif" } body { text-align: center; } fieldset { border: 1px solid #666; border-radius: .5em; width: 30em; margin: auto; } form { text-align: left; display:inline-block; } input { margin-bottom:1em; padding:.5em;} [value=create] { background:#ffffba} [value=delete] { background:#bae1ff} [value=update] { background:#ffdfda} [value=read] { background:#baffc9} [type=submit] { margin-right:1em; margin-bottom:0em; border:1px solid #333; padding:.5em; border-radius:.5em; } </style> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script> <script> $(document).ready(function() { $("form").each((i,el) => { $(el).wrap("<fieldset></fieldset>" ); $(el).before("<legend>" + el.action + "</legend>"); $(el).append("<input name=_action type=submit value=create ><input name=_action type=submit value=read >") $(el).append("<input name=_action type=submit value=update ><input name=_action type=submit value=delete >") }); $("input:not([type=hidden],[type=submit])").each((i,el) => { $(el).before("<label>" + el.name+ "</label><br/>"); $(el).after("<br>"); }); }); </script> </head> <body > <form action=/user method=post > <input type=number name=id /> <input type=text name=name /> <input type=email name=email /> </form> <form action=/event method=post > <input type=number name=id /> <input type=date name=from_date /> <input type=date name=to_date /> <input type=text name=text /> <input type=number name=user_id /> </form> </body> </html> """ router = dict({"" : lambda fo: html,}) def simple_app(environ, start_response): fo, fi=multipart.parse_form_data(environ) fo.update(**{ k: dict( name=fi[k].filename, content_type=fi[k].content_type, content=b64encode(fi[k].file.read()) ) for k,v in fi.items()}) table = route = environ["PATH_INFO"][1:] fo.update(**dict(parse_qsl(environ["QUERY_STRING"]))) HTMLtoData().feed(html) metadata = MetaData() metadata.reflect(bind=engine) Base = automap_base(metadata=metadata) Base.prepare() attrs_to_dict = lambda attrs : { k: ( "date" in k or "time" in k ) and type(k) == str and parser.parse(v) or "file" in k and f"""data:{fo[k]["content_type"]}; base64, {fo[k]["content"].decode()}""" or v for k,v in attrs.items() if v and not k.startswith("_") } if route in tables.keys(): start_response('200 OK', [('Content-type', 'application/json; charset=utf-8')]) with Session(engine) as session: try: action = fo.get("_action", "") Item = getattr(Base.classes, table) if action == "delete": session.delete(session.get(Item, fo["id"])) session.commit() fo["result"] = "deleted" if action == "create": new_item = Item(**attrs_to_dict(fo)) session.add(new_item) session.flush() ret=session.commit() fo["result"] = new_item.id if action == "update": session.delete(session.get(Item, fo["id"])) new_item = Item(**attrs_to_dict(fo)) session.add(new_item) session.commit() fo["result"] = new_item.id if action in { "read", "search" }: result = [] for elt in session.execute( select(Item).filter_by(**attrs_to_dict(fo))).all(): result += [{ k.name:getattr(elt[0], k.name) for k in tables[table].columns}] fo["result"] = result except Exception as e: fo["error"] = e session.rollback() else: start_response('200 OK', [('Content-type', 'text/html; charset=utf-8')]) return [ router.get(route,lambda fo:dumps(fo.dict, indent=4, default=str))(fo).encode() ] print("Crudest CRUD of them all on port 5000...") make_server('', 5000, simple_app).serve_forever()
No comments:
Post a Comment