Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Roadmap #396

Closed
tomchristie opened this issue Feb 18, 2019 · 18 comments
Closed

Roadmap #396

tomchristie opened this issue Feb 18, 2019 · 18 comments

Comments

@tomchristie
Copy link
Member

tomchristie commented Feb 18, 2019

I've been thinking about how best to mature Starlette as it becomes more and more capable for large-scale projects. The key thing I want the framework to focus on is low-complexity of the underlying stack.

Rather than have a monolithic application object, where we put all the functionality on, I'd like to see us focus on independent components. I think this is really important, because it makes it so much easier to understand the stack that you're building on top of. Components are independently testable, can be understood in isolation, and can be swapped out or adapted more easily.

One example here is we currently have template_dirs on the application instance. That makes it less clear what interactions the application instance might have with template dirs under the hood. Instead we should keep all the templating properly independent from other parts of the stack.

A related aspect of this is routing. Right now we promote the decorator style, which is convenient. I think we should move towards promoting a more explicit style made up of the actual BaseRoute instances that the Router class uses. The reason here is that it explicitly surfaces information about how the underlying stack function. If you want to implement an alternative to Route, WebSocketRoute, Mount, Host, it's already more clear how you'd do that. Even the call stack that gets passed through is being made more explicit. "Look at this class implementation if you want to understand what's happening here".

Here's a sketch of how a more explicit style would fit together:

settings.py

DEBUG = config(...)
DATABASE_URL = config(...)

resources.py

cache = Cache(...)
database = Database(...)
templates = Jinja2Templates(...)

routing.py

routes = [
    Route(...),
    Route(...),
    WebSocketRoute(...),
    Mount(...)
]

application.py

middleware = [
    ...
]

app = Starlette(routes=routes, middleware=...)
@taoufik07
Copy link
Contributor

I've always liked how django is organized and when working with other frameworks I try to follow django's structure.

We can add a views.py or controller.py where the endpoints will live, also add some cli to init the project.

@tiangolo
Copy link
Sponsor Member

Thanks for sharing the plans and roadmap!


A small opinion that might or not be relevant about routing:

In the same spirit of having components of related parts together, I like the decorator style of routing, because it puts in the same code region (the same section in a file) two very related parts: the path/route and the function that handles it.

Having routes in a separated section that reference functions imported from other files (more Django style than Flask style) makes the used classes more explicit, but it also makes the relationship between the function that handles a specific path and the path itself less explicit.

I guess this is some kind of tradeoff of two types of explicitness, between classes used and path/function relationship.

I know that the decision here won't affect the style used in frameworks based on Starlette (FastAPI in my case), because of the great modularized/component-based structure already in place. And again, all this might be relevant or not, but I wanted to share that idea for you to consider.

@tomchristie
Copy link
Member Author

See also 879bbcd

@threeid
Copy link

threeid commented Feb 19, 2019

I like starlette, but i stop learning at the moment.

Please add more documentation for API.
Last time i tried, i have to look at the code,
it slows down learning process.

And any chance to use YAML for configuration?

Thanks and Have a great day!

@woile
Copy link
Contributor

woile commented Feb 20, 2019

I really like this approach tom.

I started writing an app recently and I had the same feeling about routes. I wanted to add the routes as you say but when I read the docs I only found examples for HTTPEndpoint, or that is what I understood, because all of the example are with capital, e.g: Route('/users/{user_id:int}', endpoint=User, methods=['GET']). It would be nice to add examples on how to do this with function views. I didn't want to invest much time on it so I ended up doing @app.route(...).

@threeid using yaml as configuration would really fit as a plugin IMO.

I was also thinking, should starlette provide some cli to initialize a module or an app? Something like starlette init my-app and gives you:

.
├── application.py
├── resources.py
├── routing.py
└── settings.py

I don't know if it makes sense, it would become an opinionated framework, but at the same time it would be easier to organize, would be better or not? Maybe also a plugin starlette-cli which initializes an opinionated service with some defaults

@alex-oleshkevich
Copy link
Member

@woile, the idea with CLI tool is super cool! But before we have to define a file structure.

I would not afraid of being an opinionated framework.
If project has a recommended structure -- it has multiple benefits:

  1. If it is a standard/recommendation then:
    1.1. Every developer is familiar with that;
    1.2. Any starlette package/plugin has the same predictable structure;
  2. It just saves time for other developers fighting with the ideal file structure instead of writing features;
  3. At least, the file layout belongs to project. Each developer can reorganize it on per-project basis. Starlette left untouched.

@ghost
Copy link

ghost commented Mar 8, 2019

Opinionated framework may help with stability and understand-ability BUT please do not take it too far. I really like what @tomchristie says about independent components, I think we can all agree on the benefits of that. Having clear philosophy on an open-source project will make it or break it. But too much opinion in terms of "how to use" for the end-user may not be the wisest and people will start forcing their projects into a particular pattern without much forethought. Making opinions optional is always best.

E.G. with Django's "Apps" most people try to divvy up their project, but then run into issues where their models.py files are all interdependent and tightly coupled, views start needing to query models in different apps, etc. So the next thing they try is just putting everything into a single "App" and end up with monolithic files. Then within a single App try their own organization strategies before eventually re-implementing django's "Apps" and run into the same problems they had in the first place. Eventually they realize they need to go back to django's multiple apps, but out-source the coupling to the settings, a process essentially missing from the django docs.

So it's really important when giving opinions on "how to use" that you back it with philosophy and clear guidance, otherwise people waste too much time on "this is how it's supposed to be done" instead of "what's the best way to do this".

@tiangolo
Copy link
Sponsor Member

I agree that having/enforcing too strong opinions reduces the flexibility and modularity.

I think specific file structures would fit better in project generators (e.g. with Cookiecutter). That way, there's an opinionated option that people might use or not, but the basic micro framework/toolkit keeps fully flexible.

For example, Django/Flask have a lot of stuff and file structure definitions to isolate different applications in the same installation. It made a lot of sense at the time, but now there's probably a lot of people that would isolate at another level, with Docker (or serverless tools), for example.

@tomchristie
Copy link
Member Author

tomchristie commented Mar 19, 2019

Did some sketching out today of what you need in order to start getting towards a rough Django-ish feature parity.

Notable bits missing at the moment...

  • Password hashing.
  • orm needs a ModelRegistry
  • typesystem doesn't yet have any API for validate_all, or for async validators.
  • Allow RedirectResponse to take an endpoint name.
  • Make request not be strictly required in TemplateResponse.
  • Few remaining question marks over typesystem API.
  • Need a general request.parse() function, that just handles whatever media type.

Even now, it's quite an imposing chunk of code.

# settings.py
config = Config(env_file='.env')

ALLOWED_HOSTS = config('ALLOWED_HOSTS', cast=CommaSeperated, default="127.0.0.1,localhost")
TESTING = config('TESTING', cast=bool)
DATABASE_URL = config('DATABASE_URL', cast=databases.DatabaseURL, default='sqlite:///db.sqlite')
TEST_DATABASE_URL = DATABASE_URL.replace(name=f'test_{DATABASE_URL}')
SECRET_KEY = config('SECRET_KEY', cast=Secret)


# resources.py
templates = Jinja2Templates(directory='templates')
statics = StaticFiles(directory='statics', packages=['boostrap4'])
forms = typesystem.Jinja2Forms(package='bootstrap4')
database = databases.Database(url=TEST_DATABASE_URL if TESTING else DATABASE_URL)
models = orm.ModelRegistry(database=database)
hasher = PBKDF2PasswordHasher()


# models.py
class User(orm.Model):
    __table__ = 'users'
    __registry__ = models

    id = orm.Integer(primary_key=True)
    username = orm.String(max_length=200)
    password = orm.String(max_length=200)

    async def check_password(self, password):
        return await hasher.check_password(password, hashed=self.password)

    async def set_password(self, password):
        hashed = await hasher.make_password(password)


class Note(orm.Model):
    __table__ = 'notes'
    __registry__ = models

    id = orm.Integer(primary_key=True)
    user = orm.ForeignKey(User)
    text = orm.Text(default='')
    completed = orm.Boolean(default=False)


# schemas.py
class Login(typesystem.Schema):
    username = typesystem.String(max_length=200)
    password = typesystem.Password(max_length=200)

    async def validate_all(self, data):
        user = await User.objects.get_or_none(username=data['username'])
        if user is not None and await user.check_password(data['password']):
            return user
        raise ValidationError('Invalid login')


class CreateNote(typesystem.Schema):
    text = typesystem.Text()
    completed = typesystem.Boolean(default=False)


# endpoints.py
@requires(AUTHENTICATED, redirect='login')
async def homepage(request):
    if request.method == 'GET':
        form = forms.Form(CreateNote)
        notes = await Note.objects.all(user=request.user)
        return templates.TemplateResponse('login.html', {'form': form, 'notes': notes})

    data = await request.parse()
    validated, errors = await CreateNote().validate_or_error(data=data)
    if errors:
        form = forms.Form(CreateNote, data=data, errors=errors)
        notes = await Note.objects.all(user=request.user)
        return templates.TemplateResponse('login.html', {'form': form, 'notes': notes})

    await Note.objects.create(user=request.user, text=validated.text, completed=validated.completed)
    return RedirectResponse(endpoint='homepage')


async def login(request):
    if request.method == 'GET':
        form = forms.Form(Login)
        return templates.TemplateResponse('login.html', {'form': form})

    data = await request.parse()
    validated, errors = await Login().validate_or_error(data=data)
    if errors:
        form = forms.Form(Login, data=data, errors=errors)
        return templates.TemplateResponse('login.html', {'form': form})

    request.session['user'] = user.pk
    return RedirectResponse(endpoint='homepage')


async def logout(request):
    request.session.clear()
    return RedirectResponse(endpoint='login')


async def not_found(request, exc):
    return templates.TemplateResponse("404.html", status_code=404)


async def server_error(request, exc):
    return templates.TemplateResponse("500.html", status_code=500)


# routes.py
routes = [
    Route('/', homepage, name='homepage', methods=['GET', 'POST']),
    Route('/login', login, name='login', methods=['GET', 'POST']),
    Route('/logout', logout, name='logout', methods=['POST']),
    Mount('/static', statics, name='static'),
]


# application.py
events = [
    Event('startup', database.connect),
    Event('shutdown', database.disconnect)
]

middleware = [
    Middleware(TrustedHostMiddleware, allowed_hosts=ALLOWED_HOSTS),
    Middleware(HTTPSRedirectMiddleware, enabled=not DEBUG),
    Middleware(SessionMiddleware, backend=CookieSignedSessions(secret_key=SECRET_KEY)),
    Middleware(AuthMiddleware, backend=DatabaseAuthBackend(model=User)),
]

exception_handlers = {
    404: not_found,
    500: server_error
}

app = Starlette(
    debug=DEBUG,
    routes=routes,
    events=events,
    middleware=middleware,
    exception_handlers=exception_handlers
)

@jbham
Copy link

jbham commented Mar 27, 2019

Hello Tom,

Thank you for your efforts on Starlette and DRF! I have one question. With django looking to go towards async, what would that mean for starlette? I see that you don't see django async as threat to starlette. So just trying to understand how starlette and django async would fit into big picture and what's that mean for python async web framework. I apologize in advance if this sounds like a stupid question but I couldn't connect the dots.

Thanks!!

@tiangolo
Copy link
Sponsor Member

Excellent!

Just to chime in, about password hashing:

It might be reasonable to delegate password hashing to an external library (passlib), it is well tested, widely used, etc. as is commonly needed for cryptographic stuff.

I see that in Django 2.x, the default password hashing algorithm is the same as the default in passlib: pbkdf2-sha256.

So, plugging-in passlib to Starlette and family would make it compatible with the same database a Django app is using, allowing migration/interaction, etc. (I've done that, but with Flask, some time ago).

Then, there wouldn't be a need to develop another package just for Starlette or increase the size/scope of Starlette itself. It would only require documenting how to install and use it (e.g. I have it documented for FastAPI here: https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/, although I documented it using BCrypt).

And Passlib supports a lot other hashing schemes too.

Also, almost all cryptography (e.g. password hashing) is mainly CPU-bound, not I/O-bound. So, it wouldn't change making it async or not, so, you can just use it directly.

Just some thoughts 😄

@tomchristie
Copy link
Member Author

With django looking to go towards async, what would that mean for starlette? I see that you don't see django async as threat to starlette.

I'd like to see a bunch of this stuff filtering over to Django, but I think Starlette's in a position to inovate faster given that it's got so much less baggage. I think it'd be beneficial both for Django to gradually move towards an async stack, and for work to continue on pushing Starlette and the rest of the ecosystem forwards.

It might be reasonable to delegate password hashing to an external library (passlib), it is well tested, widely used, etc. as is commonly needed for cryptographic stuff.

That looks like a great resource! Thanks!

Also, almost all cryptography (e.g. password hashing) is mainly CPU-bound, not I/O-bound. So, it wouldn't change making it async or not, so, you can just use it directly.

Yes, but you do need to run it in a thread pool. Because it's deliberately CPU-slow it'll end up blocking any other async tasks from running for the duration it takes to perform the hash. If you've got a whole load of concurrent connections it'll end up introducing latency onto them. If you run the hashing within a thread-pool then the O/S can still switch back and forth between the password hashing thread and the main asyncio-thread.

@tiangolo
Copy link
Sponsor Member

Yes, but you do need to run it in a thread pool. [...]

Get it, good point. Thanks for the clarification.

@jbham
Copy link

jbham commented Mar 28, 2019

Thanks for responding @tomchristie!

@hyperknot
Copy link

hyperknot commented Apr 23, 2019

Hi, I really like the direction where this framework is going. I think #396 (comment) is quite nice, but what's missing and what's really not-elegant right now is file imports.

Do you recommend importing "database" resource in every view, and also even view in routes.py?

I'm coming from Pyramid, and I believe over the years they have arrived at some really elegant structure. No global imports, just everything passed via requests. Routes are in a separate file, like you've suggested, but views have a really powerful decorator which matches functions with routes and also handles reverse routing. Have a look at modern Pyramid, if you're looking for inspiration. I believe Pyramid is 10x closer to this framework's approach, in many regards actually! Both minimal and supporting large apps at the same time, with non-core functionalities all split into separate packages.

@LtMerlin
Copy link

LtMerlin commented May 23, 2019

Hello Tom,
I'am a Django user for quite some time now and I like where this magnificent innovative "Django-ish" framework is heading with this future proof async concept in its backbone...
Really great job!
To start migrating slowly towards using Starlette, I'm having some slowdown in learning this framework for large scale projects... Any ideas when there will be some more docs available? Like API docs from the docstrings perhaps? Then we don't have to go through the source code eacht time to figure out which params are available etc...

Thanks!

mvolfik added a commit to mvolfik/starlette-websockets-demo that referenced this issue Jan 20, 2021
@tomchristie
Copy link
Member Author

Closing as stale.

@hasansezertasan
Copy link

hasansezertasan commented May 30, 2024

First of all, I'd want thank everyone who worked on Starlette 🙏. It's a wonderful peace of tech.

Even the call stack that gets passed through is being made more explicit. "Look at this class implementation if you want to understand what's happening here".

I just wanted to point out that this statement is sound.

It's been a long time since I read the documentation for the first time (I really didn't understand much at that time) and I started reading it again last weekend.

As I developed many FastAPI applications and some basic extensions around asgi/starlette I found myself discovering starlette source code and reading stuff on the internet about ASGI, I started to notice its pattern and understand its design. Its fabulous ✨ and really shines 💫.

I was developing web applications with Flask, before FastAPI and Starlette. Back then, I loved the decorator style but Starlette made me realize that it's just for convenience. I'm happy about the depreciation decision on the decorators used to register routes, exception handlers, hosts, mounts, etc.

I also love the simplicity. Just look at Starlette class, it's not a subclass, it uses Router for routes, it automatically registers some middlewares (which are not necessary and any developer who peeked at the source code can easily replicate and override this logic). I love how the things are separated and isolated. I love the decision making on the component responsibilities.

It's simple, powerful and light.

As I said, I used to develop with Flask, which had many features (e.g request.endpoint would give the name of the route/endpoint that is requested) but wasn't easy to understand it's core compared to Starlette (which we shouldn't compare starlette with Flask but werkzeug).

Anyway, great work, great gain. Thank you 🙏.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants