|
|
12:47 |
|
show
|
3:01 |
Hello and welcome to Full Web Applications with FastAPI.<br>
Applications, that's the key word about what we're gonna build in this course.<br>
FastAPI well, it has API right in the name, it's really, really good at building web APIs.<br>
But, as you will learn during this course, it's also really, really good at building web applications.<br>
Many of the cool, modern features of FastAPI can, with a very small amount of effort, be applied equally to web applications, those that generate HTML for browsers and humans, as it does for JSON, which is intended for computers and computer software to exchange data with each other.<br>
So let's start with this really important question here.<br>
Should you be focused on building an API or a web app?<br>
In the API, we're going to build applications that exchange JSON, they deeply leverage the HTTP verbs like GET PUT POST DELETE and so on and they exchange data that is generally unseen.<br>
Just a little progress bar spinning in you're app and then data appears, or do we want to build some kind of web application?<br>
Somewhere on the Internet users go and type and they show up on your page and they've got really cool dynamic HTML.<br>
They can go over, maybe create an account.<br>
They log in, they interact through their web browser.<br>
Now, this is a challenge that basically everyone has to deal with if they're building a rich application, say a mobile app, and they're going to build a website that corresponds to it.<br>
So often this is presented as APIs versus web apps.<br>
I maybe want to build some API or use an API framework like FastAPI, so that I have this great way to build APIs And then we'll go find another way to build that web app or we've already got our web app.<br>
How do we add an API to it?<br>
Do we have to completely start over?<br>
Good news, what you're going to see in this class is that it's not "or" or "vs" or anything like that.<br>
No, it's "together".<br>
I will show you how to build APIs in FastAPI and then mostly we will focus on building fantastic web applications.<br>
We have another course that focuses purely on building the APIs, but the interesting point here is how do I take this really powerful and modern API framework and allow it to also serve web applications, web UIs out of the exact same data models, out of the exact same code base?<br>
So you don't have two things to deploy, two things to manage, two things to run.<br>
It's not like, well, let's use FastAPI for the APIs an then completely start over and use Django for your web app.<br>
No, it's FastAPI through and through, And you're gonna be able to blend them together within the same application with the same versioning, the same deployment, everything.<br>
It's going to come out with one of the best web frameworks, not just API, but web frameworks that you can use for modern Python web applications.<br>
You're gonna see how to unlock that power in this course.
|
|
show
|
3:01 |
Now let's compare FastAPI to some of the other popular Python options that you might choose.<br>
Here's FastApi's home page, it's documentation and so on.<br>
The documentation is really thorough and really good.<br>
So what else might you use instead of FastAPI?<br>
Well, there's what I think of as the big two: Django and Flask.<br>
These two combined represent about 80 to 82% of the current Python web application framework mindshare, deployments and so on, especially looking at stuff that's been built recently.<br>
Between them they are the two ways that people often think about web applications and Python.<br>
Django has all these somewhat larger building blocks that you click together to build your app whereas Flask is all about it's micro framework lifestyle.<br>
You get to pick every little thing and there's zero help for you.<br>
You want a database?<br>
Okay, go get a database.<br>
You want to talk to it.<br>
OK, go figure that out.<br>
Right?<br>
Do you decide on SQLAlchemy Do you decide on MongoDB?<br>
Do you?<br>
What do you do?<br>
So it's all about picking the little pieces and putting them together however you like in Flask.<br>
We also have Pyramid and Tornado.<br>
I'm a fan of Pyramid, I like that framework quite a bit.<br>
It's very fast, and Tornado is one of the very first asynchronous style of frameworks.<br>
It doesn't necessarily use the, at least in the early days the async and await style of programming that's popular today because it actually existed before that style of programming but these air two good options.<br>
And if you're looking at just your API, there's frameworks like Hug or Django REST framework, or so on that you might consider.<br>
So which side are you leaning?<br>
More APIs and also need a little web or more web, and maybe we'll need an API.<br>
So if we compare these, you might say, well, how does this compare to FastAPI?<br>
One way to think about it is popularity.<br>
We're gonna talk about the features that make FastAPI awesome as well.<br>
But one is popularity.<br>
If you look Django and Flask, they're quite popular.<br>
54k and 53k stars on GitHub.<br>
This represents their very nearly equal split in the mindshare.<br>
We have pyramid at 3k.<br>
Tornado at 20k, Hug at 6k.<br>
What's FastAPI at?<br>
26k.<br>
Now wait.<br>
Maybe it's not as good as Flask or Django.<br>
Here's the thing.<br>
Django is like 10, 15 years old.<br>
At this point, FastAPI is less than two years old.<br>
So in the, you know, Django's had all this time to build up people like following and being part of it and FastAPI is really, really coming on strong.<br>
As far as I can tell, it is the most popular, relatively new framework, and being relatively new is an advantage.<br>
It means that supports async and await right out of the box without jumping through a bunch of hoops.<br>
It does really powerful things with type annotations.<br>
It works with Pydantic data models that do automatic conversion and validation and on and on and on.<br>
So because it's new, it's awesome.<br>
It has all the modern Python features you would hope for, but you can see it's nearly as popular as some of these older ones, the most popular, too, amongst the Python web landscape.
|
|
show
|
3:17 |
Briefly, let's just dive into a couple of ideas, The big ideas that we're going to cover in this course you get a sense of what you're gonna learn throughout the whole adventure that we're about to embark upon.<br>
We're going to start by building our first FastAPI site and at this level it's just gonna actually be an API that exchanges JSON with some data that we compute in our API endpoint.<br>
So we'll see what it takes to build from scratch from, you know, empty Python file till we get something running on the Internet, at least running on our local server that theoretically could be on the Internet that we can interact with.<br>
So we're gonna start out and see what is the essence.<br>
What are the few moving parts that we must have for a FastAPI site and then we're going to move on to the goal of this course serving HTML.<br>
Take this cool API now, how do we also let it handle the HTML, the web application the user interactive browser side of what you also need to build?<br>
Sure, you could build some APIs and those are great.<br>
But in addition to that, how do you also build your web application?<br>
So that's what this next section is gonna be all about.<br>
We're going to see how we can return first of all, basic HTML and then how we can use a template language like Jinja or Chameleon to create these dynamic templates that actually generate the HTML.<br>
We're going to talk about this idea of view models.<br>
If you're familiar with FastAPI, You may have heard of Pydantic this are really cool ways to exchange and validate data at the API level.<br>
They're great, and I absolutely adore that technology, but it doesn't make sense for working with HTML.<br>
As you'll see, when I talk about why that's the case.<br>
But it turns out that a similar but not exactly the same design pattern is what's gonna make the most sense here.<br>
So I wanna see about using that to correctly factor HTML and the validation and data exchange and then the actual doing the logic part of our web app.<br>
We're going to work with the database, of course.<br>
So we're gonna use SQLAlchemy to map Python classes to the database and we're gonna do this in two passes.<br>
First, we're going to use the traditional SQLAlchemy, API, which does not support async and await.<br>
But there's a new API.<br>
That's coming, starting in 1.4 and then heading towards version 2 of SQLAlchemy.<br>
That absolutely supports async and await, which is going to be a really important aspect of working with FastAPI and making it fast.<br>
So we don't do that in two passes, start out with the synchronous version that you're probably familiar with and then upgrade it to this new async version.<br>
And once we have an async database in place, we can now convert our entire web application to run fully asynchronously.<br>
This means many, many, many times more scalability for the same hardware.<br>
We don't have to write as complicated software with different tiers and caching and all these different things.<br>
Our application is gonna be super fast right out of the gate, and finally, once we get this cool app built and running, we're going to put it out on the Internet.<br>
We're gonna actually go out, create an ubuntu server and deploy this to the Internet where we'll publish it, people can interact with the browser, even talk to it over SSL.<br>
These are the major ideas that we're going to cover in this course.<br>
And when we get through all of them, were gonna have a fantastic web application that you can use as an example for whatever it is you want to build.
|
|
show
|
0:58 |
What do you need to know To be successful as a student in this course?.<br>
What do we assume that you know and we don't go into while we're going through it?<br>
Well, we assume that you pretty much know Python.<br>
You don't have to know all the advanced features of Python.<br>
You don't have to be able to create weird, dynamic meta classes or anything like that.<br>
But basic things like variables, functions, loops, classes, that's the kind of stuff we expect you to know, we also expect that you have a little bit of familiarity with the web and HTTP and HTML, but we work with some HTML, some CSS and some dynamic templates in this course.<br>
So we don't go through and teach you what this part of HTML means or talk about how CSS actually works.<br>
We just use the CSS and use the HTML.<br>
If you're somewhat shaky on it, probably OK, you can pick it up along the way, but it's not a focus to this course.<br>
So we do assume that you at least can pick it up on the way or you know it already.<br>
So with those two things in place, you're ready to take this course
|
|
show
|
1:14 |
So what application are we gonna build during this course?<br>
If we go over two PyPI, we could find that we search around.<br>
There's all sorts of different projects and maybe we could build one of those, but no, in fact, what we're gonna build is pypi.org itself.<br>
That's right.<br>
We're gonna create a clone of pypi.org.<br>
This entire site.<br>
Well, much of the site anyway, we're going to build a UI That looks like this, would have data coming back from the database like that.<br>
We're gonna have featured packages that are in here like that, who have the ability to log in, to register, have accounts, all that kind of stuff.<br>
So that's the application, the web application that we're gonna build.<br>
We're going to create a clone of pypi.org.<br>
It's a great example because it talks to a database, it has a decent number of multiple pages, it has HTML forums, it has validation, has accounts.<br>
Many, many of the things that almost any web application that is meaningfully large or meaningfully real will have.<br>
You wanna build a bookstore, it'll be similar.<br>
You wanna build a forum site, it will be similar.<br>
So I think this is gonna be a great example.<br>
We're all pretty familiar with it from our experience with Python.<br>
It's not too complicated, but it's not too simplistic at the same time.<br>
And we're gonna build an awesome asynchronous version of it with FastAPI
|
|
show
|
0:39 |
you might be wondering, who is this disembodied voice talking to you?<br>
Well, here I am, Michael.<br>
My name is Michael Kennedy, @mkennedy over on Twitter.<br>
Thanks for being to my course.<br>
I'm really excited to teach it to you.<br>
You might know me from the Talk Python To Me podcast where I founded it, and I'm the host.<br>
I'm also a, founder and co-host of Python Bytes, which I co-founded with Brian Okken.<br>
Maybe even more relevant for this particular situation is I'm also the founder and principal author at Talk Python Training.<br>
I've got a lot of different exposure to different types of Python and I'm going to try to bring all that experience to this course and share it with you
|
|
show
|
0:37 |
Now, if you really diving into FastAPI, you might want to take an hour when you're doing the dishes or you're out for a walk or something and listen to one of my Talk Python To Me episodes.<br>
In particular, Episode 284 where I interviewed Sebastian Ramirez.<br>
He is the founder and creator and maintainer of FastAPI.<br>
So he and I gotta have an awesome chat about why he created it, some of the design choices, where some of the inspiration came in and how to use it.<br>
If you want to go a little bit deeper, feel free to check out this episode where I interviewed the creator of FastAPI about his creation on that show.
|
|
|
7:24 |
|
show
|
3:09 |
Before we start writing code and just jump right into our editor, let's make sure that you have your machine set up and you can follow along, and you can build these applications with the course.<br>
It's really important that you follow along.<br>
So when we do stuff in the course at the end of a chapter, stop and go back and add that to either the same application that you're building along with me or create a parallel but very similar application and add the functionality over there So in this super short chapter, what we're going to do is just go through and make sure that your machine has all the requirements and tools that you need.<br>
The first question is, do you have Python?<br>
And importantly, is it the right version?<br>
FastAPI has a minimum requirement of Python 3.6, and we're also using features like f-strings in our code that require Python 3.6 or later.<br>
So you need 3.6 we're actually gonna be using, a higher version.<br>
But make sure you have at least Python 3.6.<br>
You wanna know, do I have Python?<br>
It's a little bit complicated to tell, but here's a couple things we can do.<br>
If you're on Mac or Linux, Yu can go and type Python3 -V and you'll get some kind of answer.<br>
Either Python 3 doesn't exist, in which case you need to go get Python or make sure it's in your path.<br>
Or it might be higher version lower version, whatever.<br>
You need to make sure that this runs and that you get 3.6 or above, we're gonna be using 3.9.1 Actually, during this course.<br>
On Windows, it's a little bit less obvious.<br>
There's a few things that make this challenging if you're not totally sure.<br>
So on Windows, what you type usually is Python, not Python3, even though you want Python 3.<br>
So you say Python -V.<br>
And if you get an output like this Python 3.9, 3.9.1 or whatever as longs that's above 3.6, you're good to go, but here's where it gets tricky if your path is set up to find, say Python 2.<br>
But you actually have Python 3 in your system it's just later in the path definition.<br>
You're going to need to adjust your path or be a little more explicit how you reference that executable.<br>
And here is the super tricky part.<br>
Python on Windows 10 is not included.<br>
But there is this Shim application whose job is to take you to the windows store and help you get Python.<br>
If you don't have it yet, it will respond to Python -V but it will respond by having no output.<br>
It won't tell you that Python is not actually installed that you need to go to the store and get it.<br>
It will just do nothing.<br>
So if you type Python -V and nothing happens, that means you don't actually have Python.<br>
You just have the shim that if you took away the V, would open the Windows store for you to get it and so on.<br>
So just make sure you get an actual output when you say Python -V or, you know, follow the instructions coming up on how to get it.<br>
Speaking of getting Python, if you need it, go visit realPython.com/installing-Python/.<br>
They've got a big range of options for all the different operating systems, the trade offs, how to install it for your operating system.<br>
And they're keeping this up to date.<br>
So just drop over there, get it installed in your machine and come back to the course, ready to roll.
|
|
show
|
2:31 |
what editor are we gonna be using for this course?<br>
Well, my absolute favorite is PyCharm.<br>
I think PyCharm is by far the best editor for Python applications, and the larger and more diverse they are like web applications are set to be, the better off that PyCharm is.<br>
Now, you don't have to use PyCharm, but if you want to follow along exactly, I recommend you get PyCharm.<br>
And for this course you're most likely, going to need the professional version for some of the features, you could get away with a community free version.<br>
But a lot of the functionality, the auto complete in the HTML and CSS side won't work.<br>
You'll have to have the PyCharm Pro Edition, and I'll give you some other options if you don't want to get that.<br>
But you can get it as a student if you work with any university or high school or whatever, you can get it completely for free, even the Pro Edition, and there's also a trial, so those are some options.<br>
I recommend it.<br>
I think it's absolutely worth it.<br>
Get visited over here at jetbrains.com/pycharm/ Now, the way I would put it on my machine as I would get, or I do get the JetBrains toolbox.<br>
This auto updates it for you.<br>
It lets you jump easily between different versions, that lets you know when there's new versions I think that's really the best way to manage JetBrains tools and products on your OS.<br>
So if you do go with PyCharm, get it this way.<br>
Now for some reason, if you don't want to use PyCharm, the other really good editor that I would recommend is VS Code.<br>
I don't know that I'd like VS Code.<br>
In fact, I'm pretty sure I don't like it as much as PyCharm.<br>
But I do like it.<br>
I think it's good.<br>
It has a lot of great features.<br>
It supports the HTML and the CSS and all the different languages that we're going to be touching during this course So that's cool.<br>
You go download it for free.<br>
And if you have an M1 one Mac, there's even a way to get the M1 version, native version over here, which is cool also for PyCharm by the way.<br>
If you do get it, You're going to need to install a couple of things.<br>
You want to make sure that you install the Python extension or it won't have all the Python smarts.<br>
You also, on top of that, want to install Pylance, which gives it better auto complete and better understanding still of your code.<br>
In order to get this dialogue to come up, you press that little box icon thing there and then you get the marketplace.<br>
Python should be right near the top.<br>
It's by far the most popular extension for VS Code.<br>
So this is a really good choice if you don't want to use PyCharm.<br>
Either way, get one of these two and or pick your favorite editor that you think will work well for this course and we'll be ready to work on the code together
|
|
show
|
0:30 |
Last, but definitely not least you want to make sure you have the source code for this project.<br>
Now, we are going to come and just create a new folder and create a new file and start writing code from scratch.<br>
But there are a couple of things that you're going to need if you want to follow along, for example, see where it says data/pypi-top-100 Once we get to the database section, we're gonna load up the database with a whole bunch of data that's in JSON format that is going to represent the actual live data on pypi.org.<br>
In order to get that, you got to get to the GitHub repository or if you want to just jump into, say, chapter five and start working from there, we'll have the code that we started with and finished for chapter five.<br>
So make sure you get this repo.<br>
You're going to clone it for sure.<br>
If you're not a friend of git, you can just click on that green button where it says code and download it as a zip file, but if you do have a GitHub account and you use git frequently.<br>
Be sure to star and fork this so you have permanent access to it.<br>
Once you get this downloaded and cloned or unzipped, you'll be ready to follow along with the course.<br>
That's it.<br>
If you have Python, you've got a decent Python editor and you've got the source code repo, you're ready to take this course.<br>
Let's get going.
|
|
|
1:14 |
|
|
13:36 |
|
show
|
1:43 |
Hey there.<br>
Now that we've got all that positioning and motivation out of the way.<br>
It's time to start building.<br>
We're going to begin by building our first FastAPI site.<br>
In fact, what we're gonna do is going to take the site and build it up and build it up and build it up over time.<br>
First, it'll be just a really simple site then it'll start having things like dynamic HTML in templates and static files.<br>
And we're gonna add a database and all sorts of cool layers to make it realer and realer until we end up with something that is very similar to what you might consider a full professional web application.<br>
And the question is, well, what are we gonna build?<br>
We're gonna build something that I'm pretty sure you're familiar with already.<br>
We're gonna build a clone of pypi.org for those of you don't know pypi.org is where you go to find Python packages and libraries that you can install with pip, anything you can install with pip, as long as you used the standard mechanism, not some URL or something like that.<br>
It's going to be coming through this central package index here.<br>
So what we're gonna do is we're gonna build an application that looks like this.<br>
It's gonna have similar elements on the page.<br>
You'll be able to log in and register and get help and do other things as well.<br>
You'll be able to go to a package and see the details.<br>
Many of the things you would do with pypi.org you're gonna be able to do with our application, and we're gonna build this with basic FastAPI functionality and a few cool libraries we're gonna add on to make it working with HTML even better in FastAPI.<br>
So I hope you're excited.<br>
We're going to start small in this chapter and build and build and build until we have something that looks very similar both visually and functionally to what we have here at pypi.org
|
|
show
|
4:09 |
it's time to start writing some Python code and creating our FastAPI not API, but web application.<br>
Our Python project for a web application.<br>
Here we are in our GitHub repository and you can see we have a bunch of empty chapters of what I think the structure is going to be.<br>
And we're going to start by creating our project here, evolve it.<br>
And then when we're ready to move on to templates for chapter four make a copy from what we did here, over to here.<br>
That way you could always just jump in at any given section throughout the course in case you didn't follow along exactly, or you just want to quickly jump into a section and see what it was like there.<br>
We want to go over here, and we're going to create that project.<br>
Now we're gonna open this in PyCharm, but also show you how to get started before, just in a terminal.<br>
So I have this cool little extension called Go2Shell.<br>
That'll let us jump in here.<br>
But you could obviously open a terminal or command prompt and just cd over into this directory.<br>
So if we look here, there's just this placeholder text so GitHub would create the or git would create the project structure.<br>
Now what we would need to do there's a couple of things that make up a FastAPI project.<br>
We're going to start by just having a main.<br>
So what we're gonna do is We're going to create a main.py, file.<br>
We're also going to need some requirements.<br>
And the easiest, most common way to do that is to use a requirements.txt.<br>
Yeah, we could use poetry or pipenv or whatever, but I'm still a fan of just the requirements.<br>
We're going to create that as well, and we'll put in things like we require FastAPI, and we require uvicorn to run it and so on.<br>
And then I wanna have a virtual environment.<br>
So I'm gonna come over here and say Python, you may need to type Python3, depending which version you got.<br>
-m venv, venv.<br>
We're not gonna do this from scratch for everyone.<br>
I'm just gonna walk you through it once and now we have our virtual environment.<br>
But if we ask which Python.<br>
It's still we ask which Python3 It's still the system global one.<br>
On Windows, which is not a command, but where is a command that will tell you basically the same thing.<br>
So we need to make sure we activated.<br>
So on macOS or Linux we say dot or source.<br>
venv/bin/activate like that and notice our prompt changes.<br>
If this was Windows, we would just venv/scripts/activate.bat like so you could drop the bat that would still run.<br>
But over here, I got to say this.<br>
Now, if we ask which Python it's this one here all right?<br>
Any time you work with virtual environment, it's almost always got an out of date pip.<br>
So let's fix that real quick.<br>
pip install.<br>
Sorry.<br>
-u for upgrade pip and setuptools.<br>
Now we do a pip list.<br>
You see, we've got the latest version.<br>
Now that we have our virtual environment up and running, let's go ahead and open this in PyCharm, we're no longer going to need this on macOS you can drag the folder onto PyCharm and it'll open.<br>
On the other operating systems, you have to go file open directory.<br>
Same basic idea.<br>
You can see PyCharm's found our virtual environment.<br>
Surprisingly, this sometimes works, sometimes doesn't.<br>
So you can always go add interpreter and pick the existing one that usually finds it or if you have to, you can browse to it as well, but it looks like we're good over here.<br>
This are red because in GitHub, they're not yet staged.<br>
So our project is over here.<br>
And let's just do a quick print hello, web world just to make sure that we can run everything and right click, say run.<br>
Perfect.<br>
It looks like everything is running over here just fine So we've got our project created.<br>
Obviously, it's not a FastAPI project just yet, but this is the process that we're gonna go through for each one.<br>
I won't walk you through it again.<br>
Well, I'm just gonna do this and say, Hey, I did this set up to get our project running and start with the existing code at each chapter
|
|
show
|
5:13 |
Now that we've got our Python project set up in Pycharm, let's make it a FastAPI project.<br>
The first thing we need to do is go to our requirements here and have our requirements stated.<br>
So to get started, we need just two, and we'll keep adding onto this list as we start to bring in things like an ORM with SQLAlchemy, as we bring in static files support with aiofiles and so on.<br>
But we can start with FastAPI, and we can also start with you Uvicorn.<br>
So Uvicorn is the web server, FastAPI is the web framework.<br>
PyCharm thinks this is misspelled.<br>
It is not.<br>
So we can go over here and hit Alt + Enter and say Do not tell me this is misspelled.<br>
They're actually working on a feature, to understand these packages and not say that they're out of date or whatever, but there we go.<br>
Now we need to install these.<br>
So we're gonna go to our terminal, make sure your virtual environment is active here.<br>
I want to say pip install -r requirements.txt right.<br>
Looks like everything installed just fine.<br>
I'm not a fan of the red.<br>
Let's go ahead and commit these to GitHub here and notice over here, we've got our source control thing.<br>
We can do this to commit.<br>
It shows us Command+K on macOS is the hotkey.<br>
But I've also installed this tool called Presentation Assistant.<br>
When I click this, Note at the bottom we got this.<br>
But also, if I just hit Command+K, this would come up.<br>
So you'll see me do a lot of things with hot keys as I interact with PyCharm in general, in the code and so on.<br>
So if you're not sure what just happened, keep an eye on that thing in the bottom.<br>
Now, down here.<br>
We've got these two files.<br>
These call this starter code for our project.<br>
Here we go.<br>
Now things don't look broken with red everywhere.<br>
But how do we create a FastAPI project?<br>
Well, there's a really simple, what I would call the PyCon talk tutorial style where you just put everything into the main file and, hey, you have a whole app.<br>
Look how simple it is.<br>
And there's the realistic one where you break stuff into you know, isolating the views into their own parts, the ability to test the data exchange between the views and the templates.<br>
And you got the template folder and all that.<br>
We're going to start with the simple one, and we're gonna throughout this entire course move towards what?<br>
Maybe we should call the real world one where you actually organize a big, large, FastAPI web application.<br>
We're gonna start by saying import fastapi and we're also gonna need while I'm up here uvicorn to run it.<br>
Now, if you've ever worked with Flask, working with FastAPI, is very similar.<br>
It's not the same, but it is similar to what you would do with Flask.<br>
So we're gonna create an app.<br>
The way we do that, is we say fastapi.FastAPI And then we're gonna need to create a function that is called when a page is requested.<br>
So we're gonna gonna call that index.<br>
That's just like forward slash And what are we gonna do?<br>
Let's just return something really simple.<br>
"Hello, world".<br>
It might not do exactly what you're expecting, but we'll see.<br>
Okay, and then we need to decorate this function to say this is not a regular function, but a function on the web.<br>
We'll say app dot get.<br>
Notice all the common HTTP verbs here get put post delete so on.<br>
And then we specify the url.<br>
You can see there's an insane number of options.<br>
We only care about specifying the path, so this will be fine.<br>
Bring that up.<br>
And how is everything looking?<br>
It looks pretty good, but there's one more thing to do.<br>
We need to actually run our API.<br>
So we say uvicorn.run and we just give it the app and that's it.<br>
We've built a FastAPI web API so far as you'll see.<br>
Let's go ahead and run this.<br>
Here it is running down there like that.<br>
Hello, world.<br>
Wait a minute, Look carefully.<br>
What is all this JSON Raw data.<br>
Why does it say JSON?<br>
It says JSON, because FastAPI is most natively and API.<br>
It's here to build APIs that exchange data.<br>
So, for example, if we had said something different here like we had said The message is, Hello, world and we run it again and we look at the raw data, you can see it's returning JSON here.<br>
Okay, So one of the things that we need to do for this course is to tell FastAPI for certain parts, may be much of it, maybe just a little part.<br>
Whatever part that we want to be an HTML browser oriented web application, we need to tell it.<br>
Don't just, don't return JSON.<br>
No, no, no.<br>
Return HTML and ideally, use a nice structured template language like Chameleon or Jinja or something along those lines.<br>
But you can see we've got our little FastAPI app up and running here on locahost.<br>
All we have to do import fastapi, create an instance of it, use its HTTP verb decorators to indicate which methods are web methods and then run, that's it.
|
|
show
|
2:31 |
Now, before we call our little basic structure here finished.<br>
I do want to show you one quick little thing.<br>
Notice I was able to go over here, right click and say run.<br>
And I can also press this button and everything looks golden.<br>
But the way that we actually run FastAPI in production is not to go and say Python run this file.<br>
What we're gonna do is we're gonna say set up Gunicorn, which is going to oversee a farm of Uvicorn worker processes, and each one of those is going to run this application.<br>
And that way, if something goes wrong with one, it can be restarted.<br>
You can do scale out.<br>
There's all sorts of cool advantages to that.<br>
And that's generally how Python web apps run in production anyway.<br>
And when we do it in that mode, the way we're gonna have basically implicitly behind the scenes do that chain.<br>
I talked about what's gonna happen is, we're going to say uvicorn main thing here.<br>
What is it that we're working with?<br>
And then what is the name of this thing here, which is app in our example.<br>
So we're gonna go to the main module and find the app instance and run it.<br>
Watch what happens if I try to run it here.<br>
It says whoa, whoa.<br>
You're somehow like double running it.<br>
I'm not really sure what's going on here.<br>
And that's because this is really just for testing, and we need a different way to run it in production.<br>
So we're just gonna quickly fix that and say:only if you try to run it here in dev, do we want to do that.<br>
So notice this, a live template is gonna expand out to if the name is main.<br>
This is the Python convention.<br>
Say this file is being run directly rather than imported and else I don't know.<br>
This is what we're gonna end up doing in production.<br>
There's actually some things will ultimately need to do here.<br>
But for now, there's just nothing else happening.<br>
If I run over here through this mode, you can see it's running fine.<br>
And if I go into the terminal now and I try to run it like this hey, look, it runs fine as well.<br>
Okay, so you'll see that as we get this little bit more complicated.<br>
Think more things going on, like setting up the database at start up and so on.<br>
There's gonna be some interesting balancing act that we have to do around this but I wanted to make sure that you I focused on this specifically because it's easy to just say uvicorn run, but then seems like it works and then it doesn't work in production.<br>
And that's why, right, this is fine when we press the run button but in production were running through a completely different chain of events that uses uvicorn module:variable_name.<br>
So I want to make sure we get this set up right from the start.
|
|
|
53:10 |
|
show
|
1:13 |
In our previous chapter, you saw we built a simple FastAPI, API more or less, returned JSON.<br>
And our goal now is actually to exchange HTML.<br>
We may also want to build APIs with FastAPI, I mean, that's one of its main purposes.<br>
But if we're gonna have web pages that talk to browsers, we need to return HTML.<br>
Now, the last thing we want to do is write static HTML in strings in Python and then return it.<br>
No, no, no.<br>
We want to use one of these dynamic templating languages like Jinja or Chameleon or even similar to what you have in the Django templates, where we write HTML and we put a little bit of scripting in the HTML and the scripting varies by templating language.<br>
But the general idea is we put some scripting or some conditional stuff into our HTML.<br>
In FastAPI we're going to create a dictionary and hand that off to this template and say: render all these items and here's the pieces of data that you're going to need to work with.<br>
So that's what we're gonna do in this chapter.<br>
We're going to start really simple and just see how to return HTML.<br>
Then we're gonna leverage, a cool library that allows us to basically do what I just described: create a dictionary and just hand it off in the simplest possible way to under one of this underlying templating languages
|
|
show
|
4:07 |
Here we are in chapter four and in our GitHub repo I just want to give you the lay of the land.<br>
We completed all of our code in chapter three, and now I made a copy of that into chapter four.<br>
So the way this is gonna work is the final code for chapter three is the starter code for four.<br>
The final code for four is the starter for five, and so on.<br>
I've already done all the set up.<br>
Like I said, we're not going to go through that again.<br>
So let's just jump into it over here.<br>
Now, you would be forgiven to think that FastAPI is really just for building APIs.<br>
And if you wanna have a proper web app that has HTML and bootstrap and CSS or Tailwind CSS, whatever you want, that you need another framework like Django or Flask or something.<br>
Because remember, when we run this, we look at it, what we get back is JSON.<br>
And if we look in the network stack what we get over here when you do a request, the response type on this one Its content-type is application/json.<br>
Of course, it seems like, well, what this does is it returns JSON and that's true.<br>
But what we can do is we can actually change how it works.<br>
Let me introduce you to the HTTP responses.<br>
So over here in FastAPI, we have a whole bunch of other things we can do than the default.<br>
The default, of course, is to return JSON.<br>
But if we create a response under FastAPI responses and you look, we have HTML response, a file response, a redirect, JSON.<br>
That's what we're getting now, plain text, streaming content and so on.<br>
What we're gonna do is going to create and HTML response, and we're just going to say the content is some local variable.<br>
Notice all the other defaults are fine, the header defaults are fine, the status code of 200 is fine and so on.<br>
I'm gonna come over here, and for the moment, just for a moment, I'm gonna do something bad and type some inline HTML.<br>
This is not the end goal.<br>
But in here let's say we want our page to look like this.<br>
Here's some HTML and here will say: "hello FastAPI Web App", and we'll have a <div> and then we wanna make sure we close our </div> and it's gonna be: "This is where our fake pypi app will live!" And instead of returning this dictionary, we're going to return the response, and the response is gonna tell FastAPI you know what this is actually not JSON, it's an entirely different thing, like a file or in this case, HTML.<br>
We can actually just inline this right there, like so.<br>
OK, let's run it again and see what we get.<br>
Look at that.<br>
"Hello FastAPI Web App This is where our app will live" And if we go to our network and we look again at our content-type down here somewhere, there it is: text/HTML.<br>
Like all friendly HTML pages, its text/HTML and utf-8.<br>
Pretty cool, right?<br>
And if we go to View Source this time, it's well, exactly what we wrote.<br>
Is it valid source?<br>
Not really.<br>
We didn't put a body and a head and all that kind of business in there, but this is enough to show you how we can return HTML at a FastAPI.<br>
That said, this is not how you should be returning HTML out of FastAPI.<br>
We're gonna do what all the other major frameworks do: That's we're gonna use dynamic web templates, gonna put those over there, and we're going to pass a dictionary like we did before, off to that template and the template engine will generate our HTML and our static content and our dynamic content, all those sorts of things.<br>
So we're not going to do this, but this is the underlying mechanism by which we're gonna make that happen.<br>
We're gonna use another library that's super cool, in my opinion, I really, really enjoy working with it, and it makes it super easy.<br>
We're just gonna put a decorator up here that says use a template, and magically, instead of being an API, it now becomes a web view method or web endpoint here.<br>
All right, so this is what we're gonna do for this chapter: we're going to start like this, but we're gonna build up into moving into those templates and serving static content and things along those lines.
|
|
show
|
1:30 |
With Python, we have a choice of different template languages.<br>
So what I'd like to do in this section is talk about three of the main possible choices that you might choose for this dynamic HTML templating that we're gonna do and see the trade offs, and then we have to pick one and go with it for the course.<br>
So we're going to do that, and I'll give you the motivation for doing so.<br>
But let's go through three popular ones.<br>
If you've ever done Flask, you've done Jinja.<br>
So, Jinja is really the one templating language that is deeply supported by Flask.<br>
There are many other frameworks that use Jinja as well.<br>
A lot of different, maybe smaller web frameworks that are not quite as popular, that also happen to leverage Jinja, so Jinja very well may be the most popular, most well known choice.<br>
Mako is another template language and looks quite similar to Jinja.<br>
I think I actually like Mako a little bit better, you'll see that there's a little bit less: open, close, open, close all of these things you have to keep writing; and it's, here's a template line and then you just write some Python.<br>
That's pretty nice.<br>
And then we're also going to talk about Chameleon.<br>
Chameleon is unique because it doesn't leverage directly writing Python as much, but it uses more of an attribute driven way of working with the HTML so you might pass some date over.<br>
There might be an attribute that says: if this condition is true, show or hide this element that has the attribute, whereas Jinja and Mako would actually have an if statement directly in their HTML, so there's drawbacks and benefits to each one of these as we will see.
|
|
show
|
6:22 |
I think the best way to get a feel for these three different template languages and which one might be the best to choose for our application, would be to compare them with a simple example.<br>
So what we're gonna do is, say, given a list of simple dictionaries or simple objects, each of which has a category in an image.<br>
We would like to show these in a simple, responsive grid.<br>
So this comes from a bike store and would look something like this: would have comfort bikes, speedy bikes, hybrid bikes, folding bikes and so on.<br>
And what we want to do is generate this HTML results, and we're going to do it with these three different template languages, given this data on the left here.<br>
So how would this look in Jinja?<br>
Well, the first thing we have to address is what if there's no data?<br>
So either the list is not there at all.<br>
It's None, or it's just an empty list.<br>
In that case, we want a simple <div> that says "No Categories".<br>
So that's the first block, and what you should notice here is you have curly percent.<br>
If statement close curly and then annoyingly curly percent endif curly percent, there's a lot of this open close.<br>
Like if this and if for that and for and so on.<br>
So you see a lot of that in Jinja.<br>
Alright, so we've got this categories up here and this is standard Python.<br>
If you can read Python, you can read this, right?<br>
Then, if there are categories we want to do a for in loop over them and repeat a <div> that has two things: it has a hyperlink that wraps an image and it has a hyperlink that wraps just the name.<br>
So we're going to do a four c in categories in this bracket percent thing and then in the for, we have the HTML and wherever we have a dynamic element from our loop, we have c and then we'll say go to the name lower case the name just in case because that's how our routing works, let's say.<br>
Over to image then we have just the name properly cased shown there.<br>
OK, so this is what it looks like in Jinja.<br>
It's not terrible.<br>
If you know Python, you know it pretty well.<br>
Here's what it looks like in Mako and the only real difference that you'll see here is that instead of having curly percent closed curly percent and closed percent curly, you just have percent on one line.<br>
And to me, honestly, this is not a very nearly as popular as Jinja but I think this is actually a pretty neat language because it's got the same functionality, but you just write your symbols, which I think is kind of the Pythonic way.<br>
One difference is the way you get strings from objects.<br>
So instead of saying curly bracket curly bracket variable, you say dollar curly, variable.<br>
So that's how it looks over here.<br>
One thing that's annoying, it's that dictionaries don't get dot style traversal.<br>
You have to say, you know, call the get on them and so on.<br>
Right.<br>
So this is Mako.<br>
Third one is Chameleon.<br>
Now again, we're gonna have these two conditions.<br>
The top condition is we want a <div> to be shown if there's no categories.<br>
The thing to take away from Chameleon, the Zen of Chameleon, really, is that it has this template attributes language t-a-l or tal.<br>
And the way you program it is to not write Python, but to write small, small expressions as attributes.<br>
So here, if we wanna have a <div>, that's only shown when there's no categories, we say <div> no categories, and then we put this attribute that says tal:condition.<br>
And here we put basic Python, not categories, you call functions, do all sorts of Python things in here.<br>
That's how we do that part.<br>
Then the next part, we're gonna have the opposite condition.<br>
So just tell tal:condition="categories" and then we want to do a loop like before So we say, <div tal:repeat="c categories"> and then it's gonna repeat that entire block, the <div> and the two hyperlinks, each time it sees an element in that category, then it's gonna write it out.<br>
Here we use dollar curly brace like we do in Mako, but it does get the dot traversal of even dictionaries.<br>
All three of these, they're pretty decent, right?<br>
It turns out, Chameleon is really by far my favorite language.<br>
There's two primary reasons I think this is a much, much better language, even if it's not as popular.<br>
I think it's much better than Jinja.<br>
One, you don't write symbols over and over and over again.<br>
You don't say curly percent test closed, percent closed curly, curly percent and if close percent curly and just that stuff all over.<br>
But the other drawback of writing code like that is what you get is not proper HTML like this.<br>
This is proper HTML, there might be attributes that don't make any sense to a standard web browser, but if I were to load this up in some kind of tool and I try to look at it or I hand it to a designer, they would be able to look at it straight away and go: yep, I know what this is.<br>
This is HTML.<br>
So I think the fact that it's basic HTML and you don't put a bunch of code in there, I think that makes it really quite nice.<br>
The other one is, and I think this one you could be split on is a lot of people see that they could, they cannot write arbitrary Python here, where, over in say Jinja, there's ways to write arbitrary Python code in your template.<br>
Some people might see that as a drawback, like I can't do all this cool stuff in my template.<br>
To me, it's a feature.<br>
It means I can't put logic, complicated Python logic into my HTML.<br>
Where does that belong?<br>
I don't know.<br>
Not here.<br>
It belongs somewhere else.<br>
We're gonna talk about where it belongs in our web application we'll build later.<br>
But limiting the amount of Python code that we can write over here, to me is a feature, because it means you have to have more professionally, more properly factored applications.<br>
You guys can decide to use whichever one you want for this one, we're going to use Chameleon because it's proper HTML and it doesn't let you write arbitrary HTML throughout it.<br>
It doesn't have all these symbols everywhere in terms of the open close and all the Python code that's interlaced and so on.<br>
I think this is a great language, we're gonna use it for our course.<br>
What I'm going to show you, there's a very, very, very small change to make Jinja do the same thing So if you prefer Jinja, you can use that, we're gonna use Chameleon for the reasons I just laid out here.
|
|
show
|
6:28 |
Well this was nice here, to render this, but that's not how we want to write our code, is it?<br>
We want to write our HTML in an HTML file and then write our Python in a Python file and then put those together.<br>
That's how all the common web frameworks work.<br>
So let's go and create one of these templates over here.<br>
We're gonna start by creating one.<br>
This is the whole HTML page, and then we'll work on some nice shared layout.<br>
I'm gonna create a folder directory called templates.<br>
And then in here, I'm going to HTML File called, what I like to do is, I like to name my files the same as the view methods.<br>
So if the view method is index, I'm gonna name the file index and this will be Fake PyPI like that and let's just put something similar, but not exactly the same.<br>
We have <h1> now we've got a nice editor for working with HTML, beautiful.<br>
Say, call it "Fake PyPI" for now and <div>, we'll just put, actually, let's put an unordered list of the popular packages so we'll have a <ul>, it's gonna have an <li> it's gonna have three popular ones.<br>
We could do this short, cool little expansion thing in PyCharm if you hit Tab.<br>
So let's say we wanna have fastapi, we have uvicorn and chameleon.<br>
And let's also add one other thing here, let's say we wanna have a <div>.<br>
Let's put the user name and this is gonna be something I'm gonna pass.<br>
A piece of dynamic data is gonna be passed.<br>
We'll just have user_name like this.<br>
And in Chameleon, the way you say output a string from a variable, Turn that variable into a string, you say: dollar curly braces.<br>
If you're familiar with Jinja than it would look like this.<br>
But in Chameleon, you do it like that.<br>
All right, so we want to render this.<br>
How are we going to do it?<br>
Well, turns out Chameleon is not built-in in FastAPI.<br>
You wanna work with Jinja?<br>
There's actually some built-in stuff, but I'll show you a better way I think anyway.<br>
So let's go look at an external package here.<br>
Let's go over here to GitHub.<br>
Here's a cool package I created, I don't like it because I created it, I like because I really wish that it exist and it didn't so I created it for FastAPI So the idea is, it's called fastapi-chameleon and what it is, is a decorator that you can put onto your view method that will take a dictionary plus a template and automatically turn that into HTML.<br>
What we got to, we've got to install it, right now It's not yet on PyPI.<br>
Check back here when you're watching this course, there's a good chance I'm going to publish it to PyPI, but for now, just install it this way.<br>
So the way it works is you've got a template directory and I like to actually name a little more structure here.<br>
We'll get to that in a minute, but got some template file.<br>
We're gonna set this overall path of where those live and then all we gotta do is just say, Here's a template and return a dictionary and we're good to go.<br>
If you return any form of response like an HTML response or redirect response, it just ignores the template idea and just says, return that response directly.<br>
So let's go and use that over here.<br>
We're gonna install it like this.<br>
And PyCharm says: Oh, you better install it like OK, super.<br>
It's installed.<br>
And then when I go over here and I'm just going to say @template and we need to import that from fastapi_chameleon.<br>
I like that up there.<br>
Now I'm gonna first type out the template file to be index.html and we don't need this and we don't need that.<br>
All we gotta do is return some piece of data.<br>
Maybe this comes from the database or something.<br>
Will say user_name is mkennedy or something like that.<br>
And and that's spelled ok.<br>
And we try to run this.<br>
It's not gonna love it, I don't think.<br>
So when I click on this, it gives us an error.<br>
That's unfortunate, what happened?<br>
It says you must call this initialization thing first.<br>
So what we've got to do before we start using it, and we're gonna organize this better in just a minute.<br>
Is we need to go to I guess we need to import this as well, directly.<br>
And we'll go to this and we'll say global_init() and we have to pass along the folder "templates", that's what we're calling it.<br>
And I think that should do it.<br>
If we have the working directory right.<br>
If the working directory is not in the same place, you probably need to pass a full path here.<br>
Let's try it again.<br>
Fingers crossed.<br>
Look at that.<br>
How awesome is this?<br>
So here's our HTML and this is the dynamic data that was generated and we passed this over.<br>
We could actually do something like this.<br>
We could say this has a user which is a string.<br>
OK, come over here and say it's going to be user if user else "anon", something like that, OK.<br>
And we try this again.<br>
Ah, yes, it needs a default value, I guess that's the way that works, I forgot, equals, just do it like this.<br>
"anon".<br>
There we go.<br>
So if there's nothing passed, it's anonymous.<br>
But if we say user=the_account, be whatever you want.<br>
So this part is totally dynamic, as our dynamic HTML is.<br>
And this is our static stuff that we can write so so cool, so super cool.<br>
This lets us come over here and just specify a dynamic template.<br>
A Chameleon template in this case, you render.<br>
If for some reason you don't want to use Chameleon and you prefer Jinja.<br>
Check this out over here, scroll down a little bit.<br>
This friendly guy over here, cloned this project and created a Jinja version.<br>
So all you gotta do to use it is go and put a decorator or your Jinja template on as well.<br>
So use one or the other, depending on the template language that you would like.<br>
But I really like this style of let's just put this here and return a dictionary and let the system itself put the pieces together and build the HTML.
|
|
show
|
11:16 |
Let's pause building our application for a minute and actually do a little bit of organization.<br>
What's going on here?<br>
We have got our one file where everything is in here and right now it's only 20 lines of code.<br>
It's no big deal.<br>
But as this grows, is going to get larger and larger and larger.<br>
We've got to read stuff from configuration files or secret files, for our passwords potentially like to our database connection.<br>
We've got to set up the database connection.<br>
We're already setting up the templates.<br>
Maybe we're setting up other things, like routing and so on, as you will see, and it's gonna be a real mess for a real application.<br>
So what I want to do is first sketch out what we might expect to build here.<br>
So we're gonna go over here.<br>
I want to say, @app.get there mostly gonna be, but not all, gets.<br>
I'm gonna go over here and have an about, let's just have these be empty for a moment.<br>
We're gonna have this about function and like these are the sort of the homepage overall type of things.<br>
But we're also gonna have, say, account management.<br>
So let's go over here and have a an index for the account.<br>
Although in this case, because it's in the same file, we'll maybe call it not index, but you'll see that we don't really want to.<br>
For the url it's gonna be "/account" And we're gonna have a way to register for the site.<br>
So this could be "/account/register".<br>
This will be register.<br>
We're actually gonna have one to accept a get and one to accept a post.<br>
Talk more about that later.<br>
Also gonna have a login and we're gonna have a logout and so on.<br>
As you can see, this has started to get really not so nice.<br>
And this is just scratching the surface.<br>
Also in our templates over here we have a big pile of dough.<br>
So what I want to do is just take a moment and talk about reorganizing these for a larger application that looks like this.<br>
What I'm gonna dio is I'm going to go and create a folder for all of our views and call it views.<br>
And then I'm gonna further organize this.<br>
Say well, over here we're gonna have the home views and over here we're going to have Let's say the views that go with account.<br>
We're also gonna have stuff about packages like package details, package list, search package and so on.<br>
So I'll call that packages like this and so on.<br>
And then what we're going to do is move some of these over.<br>
So, like these two right here, this should be about.<br>
That's it.<br>
Let's move those over into home.<br>
You're gonna need to import some stuff like that.<br>
Notice this is a problem.<br>
Will fix this in a second.<br>
We have our account things.<br>
So here's the three about account, like that.<br>
We're also gonna need that same template thing.<br>
So I'm gonna go ahead and just rob that from up here.<br>
So we got it.<br>
Put that at the top of account as well and packages we'll put some stuff in here eventually.<br>
OK, but this is a problem, isn't it?<br>
If I try to run this, I don't think it'll crash or anything.<br>
It's just gonna give me 404.<br>
That's weird.<br>
Why?<br>
well, remember the way it worked is we load up this main file and everything that was listed there.<br>
I left my logout.<br>
Everything that was listed here before we had run, got registered.<br>
Well, now we're not doing that anymore.<br>
So what we need to do is use something different and FastAPI has a really great way to deal with this.<br>
We need to import FastAPI and then what we do is: we create this thing called API router.<br>
So we say router = fastapi.APIRouter() like that.<br>
And what this lets us do is it lets us build up the various routes or roots for my British friends.<br>
Do basically whatever you would have done with app.<br>
You just do that here.<br>
So we've got our get, we've got our post and so on.<br>
And then later we say, well, router, everything you've gathered up, install that into the application.<br>
So we're gonna do that everywhere we were using app Perfect, those were all good.<br>
And let's go ahead and do this for packages as well.<br>
OK this are all done.<br>
If we run this.<br>
Nothing magical has happened.<br>
It's still going to be that same "404 Not Found" that we saw right there.<br>
Until we go over here and we say from views import home and account.<br>
Ypu can put this all in one line if you want.<br>
I kind of like to have it separate.<br>
We're gonna import this and then somewhere along the way, we need to go to the app and say include_router and the order here might matter.<br>
So make sure you do in the order you want.<br>
Do home, we do account, then we do packages, clean up on that.<br>
Not gonna need this template business over here anymore.<br>
Now, if I run it, it should be back to good.<br>
Let's try.<br>
Tada!<br>
perfect.<br>
We got our anonymous user sort of passed in.<br>
This we should we go back and we do user=abc.<br>
Now we get our abc user right there.<br>
Okay, So what this is gonna let us do is this unlocks the ability to build much, much larger applications.<br>
We can put all the stuff we need in the home here.<br>
We do all the complicated account stuff over here, and it doesn't all get jammed into the same file.<br>
Now, corresponding to this, let's go over and organize this a little bit.<br>
Because when you see index, well, is that the index of view for home?<br>
Or is that the index view for help?<br>
I don't know.<br>
So I'm gonna make subdirectories here, and what I'm gonna do is I'm gonna name them exactly the same as the view module.<br>
So home, account and packages.<br>
Perfect.<br>
And I'm gonna take this and I'm gonna put it in home.<br>
Now, PyCharm may have fixed this.<br>
Maybe not.<br>
No, it didn't.<br>
So what we need to do is go over here, say home/ like that, go templates and then whatever goes there, let's run this one more time and make sure everything's hanging together.<br>
Perfect, still works.<br>
So this gives us a lot more organization.<br>
Now for in the home views.<br>
We know exactly where all the dynamic HTML is going to live Let me do one quick change.<br>
Here, let me rename this to pt.<br>
I renamed in here and PyCharm found that and named it over there because the way we're naming this, we actually, our template has a convention template decorators, a convention that will look at the module name: home, and it will look at the method name: index.<br>
And it says, Where's the template folder?<br>
Well, let's go look for home/index if you don't specify anything here.<br>
So we can actually omit that in this case.<br>
And look, it still works.<br>
Cool, huh?<br>
So very, very nice that it will go and find that over there for us.<br>
One final thing to address over here is this.<br>
I don't really like the way this is working here.<br>
So what I like to do is make this a little bit more clear.<br>
Because, as I said, it's going to get more complex and more complex and so on.<br>
I want to define an overall function here.<br>
I'm gonna say define a function called main and main is going to run this.<br>
And I want to define a function called configure and actually configure, at the moment can just do all this and here we'll say configure.<br>
So the idea is: there's a bunch of different pieces that we're gonna be putting together here and it'll make a little more clear where to go find those pieces.<br>
Like, for example, in the Talk Python Training equivalent of this file the only does what.<br>
The kind of stuff you're seeing here.<br>
I think it's like two or 300 lines long.<br>
You want some organization, in the end.<br>
So one thing to be aware of, I told you about in production, what gets run is the else stage.<br>
This part.<br>
We wanna make sure that we still call configure over here.<br>
Otherwise, in production you have no routes.<br>
Nothing.<br>
It won't be good.<br>
The other thing is, I wanna have more partitioning here.<br>
So I wanna make that a function and that function.<br>
So we come over here and say, extract method, say configure_route and this one.<br>
I know right now, simple.<br>
But let's go ahead and make a method out of it configure_templates because, you know, maybe you've got to do a little more work to find this thing.<br>
And there's some path juggling and whatnot.<br>
This feels to me a lot cleaner.<br>
I could come in and say OK, well what do we do for the main?<br>
When you run the app, you say I'm gonna configure it.<br>
I'm gonna run it with uvicorn.<br>
We could even go and be more explicit.<br>
Say the host="127.0.0.1" I wish you could say "localhost".<br>
I don't think you can.<br>
And then I want to configure the template and configure the routes and then either run it directly or for running in production.<br>
Just set it up, but don't actually call "run".<br>
Let's just make sure everything still works.<br>
Should be unchanged with that refactoring.<br>
And it is everything is still working.<br>
OK, so this gives us a really nice convention for the way that main works and using the routers we're able to partition out the different pieces.<br>
You very well may have APIs as well.<br>
So you might have an API directory like this and then a views directory like that.<br>
That's what I typically do.<br>
But I don't think there's any reason for us to write an API in this example course because I'm keeping it focused just on the website.<br>
But in a real one, you probably have both.<br>
I think PyCharm has special support for templates and for some reason, I think it marked it up there.<br>
If we go and say unmark is just the name, it automatically grabbed that one.<br>
We can come over here and say mark directory as template folder and it says, hold on, hold on, hold on.<br>
For this project, we don't know if you want to use Jinja or if you want to use Chameleon or Django templates.<br>
So let's go set that.<br>
Now, what would be nice is if you said yes and it took you to where that's supposed to be.<br>
For some reason, it doesn't do that.<br>
So you got to go over here and type template language and wait a second.<br>
I noticed over here it says None, and it gives you all these different template languages.<br>
I'm gonna say we're doing Chameleon, so that will give us extra support for Chameleon syntax.<br>
But now, if I go over here, I can type things like tal: repeat, whatever, right?<br>
This is the syntax that will be working with for Chameleon.<br>
And now you can see we get support in the editor for it.<br>
To me, this is a much, much more professional looking application that's ready to build a real, complicated app that is easy to maintain over time
|
|
show
|
2:24 |
One really important thing you want to keep in mind when building web applications is how are you gonna have some shared look and feel, some common look for your application.<br>
Let's take the Talk Python To Me podcast website for example.<br>
Here's the home page you can see we have the navigation across the top.<br>
We have this Linode sponsorship bar and some general styles.<br>
There's, in fact, a bunch of other stuff going on, like CSS, like JavaScript like analytics and whatnot that you don't even see here as well.<br>
But obviously the calm look and feel, it's the same, right?<br>
Over here, go to the episode page, similar navigation along the top, similar style sheets and whatnot.<br>
Going to a particular episode, Surprise!<br>
Same banner, same navigation, same style sheets and so on, maybe some extra ones for this particular page.<br>
But in general, it's the same.<br>
So having this one look and feel for your site is really important, and what we want to do is we wanna make sure that we write a single HTML page, put that code into one place and use it throughout the entire site So when we make a change, let's say we want to change that banner or we want to add a new navigation element, we change it in one place and everything changes.<br>
So there's all sorts of benefits to having these layouts that are shared.<br>
It's not just the look and feel.<br>
That's the obvious thing.<br>
But you have much more consistent things, like a consistent title.<br>
Or if you've got meta description or meta other info in the head of your document, that will all be the same and shared, and if we want to make a change.<br>
You do it in one place.<br>
You have consistent CSS and JavaScript files not just having the same ones, but in exactly the same order all the time.<br>
Consistent analytics.<br>
So if you want to do something like have Google Analytics that tracks people on every page, I don't do that in my sites with Google Analytics.<br>
But if you want to have something like that, you could add that, say at the bottom of your shared layout and every single page is always gonna have the same analytics.<br>
This one's slightly less obvious, but it's really important.<br>
You have a structured way for every other page to bring in it's extra CSS.<br>
So imagine that that episode page had an extra CSS file, an extra JavaScript file I wanted to bring in.<br>
You could have certain little parts in that shared layout That's here's where that page can bring in its extra files.<br>
You can do that in a really consistent and structured way, so these are super useful and we'll see how to add them now.
|
|
show
|
6:14 |
So we just talked about how important it is to have a shared common layout for the entire site.<br>
You wanna add a CSS file to the parent site, put it in the layout.<br>
You want to change the title?<br>
change it in one place.<br>
You want to add some JavaScript?<br>
What's, includes over there.<br>
So that's really, really important for having navigation for common look and feel as well as those other things.<br>
So let's go and do that here.<br>
So what I'm gonna do, I'm gonna start out first by creating a real simple version that we can see.<br>
And I'm gonna drop in the actual version that we're gonna work with.<br>
So I'm gonna create a place called "shared" over here.<br>
And in here I'm gonna create a file, an HTML file called "_layout.pt" My convention is: an underscore means you're never really supposed to use this directly, but it's supposed to be used as part of building up something larger.<br>
Then I'll call this "PyPI site", and then we wanna have some content here.<br>
This could also have a footer.<br>
"Thanks for visiting".<br>
Now in here, let's have some content in our site.<br>
This is just a CSS class that lets us maybe style it or add padding or something like that, because we could also have, like, a <nav> section or whatever.<br>
And in here, I would like to say: other pages, you can stick your content right here.<br>
Everything else will stay the same.<br>
But you control what goes in here.<br>
So the way that we do that in Chameleon is we do what's called defining a slot.<br>
So let me just change this names.<br>
They have nothing really to do with each other, other than, like, conceptually, you're styling this thing.<br>
We've got our main content CSS and we're gonna call it content from the other pages.<br>
And if they don't put anything here, it's just going to say no content.<br>
All right, But if they do this whole <div>, it's gonna be replaced with whatever that page wants to do.<br>
So let's go and change our home over here.<br>
Not do all of this stuff up there, not worry about this, not worry about that.<br>
It's just going to do this one thing here.<br>
So what, we're gonna do is gonna have a <div> and put that stuff into there.<br>
That's the way Chameleon likes to work.<br>
Always likes to be valid HTML.<br>
Now, in order for us to use this, what we need to do is we need to go over here and use this template language.<br>
Notice here there's "metal", we've got tal and then metal, the metal one is about these templates and so on tal is a temporary attribute language.<br>
So what we gonna do is we're gonna say "use-macro" and the easiest way to do this is say load:, and we give it the relative path from this file over to shared over to "_layout.pt".<br>
What do you get named pt.html?<br>
Let's rename that.<br>
There we go.<br>
layout.pt, could be HTML, could be pt, "pt.html" is kind of weird.<br>
OK, so there and then we need to go over here and say, there's a section that has this content and it's gonna be metal:fill-slot="content" and we don't want this tag to be part of this.<br>
Will say tal:omit-tag="True".<br>
So just this content is going to go into that hole.<br>
You want to tighten this up a little bit.<br>
All right, that should have no real change.<br>
Our code should still run.<br>
What's gonna find this index.pt when it starts to render through Chameleon, Chameleon will read this, go well, we've got to go get the overall look and feel from here and just to make it, I don't know, feel like it's got something going on, let's put a little style in here.<br>
body background.<br>
Let's make it a really light green.<br>
We do that by something like that, just so you can see some sort of common look and feel being specified here.<br>
We run it.<br>
We should get this new view.<br>
Must have made some kind of mistake, and I have not.<br>
Awesome!<br>
We've got our PyPI site and we've got our footer and then this comes from that particular page.<br>
Let's go.<br>
And not you, View Page Source, let's view the whole source.<br>
There we go.<br>
That was cached or something.<br>
So this comes from the overall layout page.<br>
This comes from the overall out page layout page, and this is what our index had to contribute.<br>
Well, with that in place, let's do a real quick navigation thing.<br>
Actually, here, let's just do a <div> that has two hyperlinks.<br>
One is going to be slash home and one is going to be about.<br>
We'll do better navigation in a minute, and let's go and add one of these for about so these are views.<br>
It's down here.<br>
We're gonna add another template.<br>
And if we call this in the home folder about.pt, it'll automatically find it which we will.<br>
Now the way I find the easiest to create new pages is just find a simple one of these and copy and paste it because the top part is always the same she'll say about and this will just be about PyPI something like that.<br>
If we run this again.<br>
Now, we should be able to jump around.<br>
If we go to home, we're here.<br>
If you go to about, about PyPI.<br>
You can tell the formatting is totally wonky.<br>
We need spacing and whatnot, but check that out.<br>
We've got our common look and feel We've got our styles coming across.<br>
We got our footer very, very cool, right?<br>
So this is how we're going to create this common look and feel over here in our FastAPI web app.<br>
Well, really, we're just leaning on Chameleon.<br>
OK, this overall common look and feel here.<br>
And then within one of these, we just say, here's the overall layout and then we're gonna fill various slots.<br>
You can have multiple ones of these, like we could set the title and so on.<br>
But we don't really need to go into that right now.<br>
This should give you more than enough to know what to work with here.<br>
So I think this is a really, really good idea for building maintainable apps because you define your layout once, and then you're good to go.
|
|
show
|
3:53 |
You might be thinking: hey, this belongs in a CSS file so that you could maintain it separately, just like we did with our HTML and Python.<br>
Maybe you wanna I don't know, have an image that you show on your website or use JavaScript.<br>
Well, those would be files in a static folder that gets served directly, not through some sort of processing that FastAPI might be doing.<br>
And some frameworks come with this built-in or defaulted like Flask automatically you can go to /static and just serve stuff out of there.<br>
FastAPI doesn't work like that.<br>
You've got to explicitly enable static files in two steps or three.<br>
So what we're gonna do is gonna create a folder, directory called static.<br>
And in here, let's just create something simple.<br>
We'll do a "site.css".<br>
In our site, let's go and move this over.<br>
<style> here.<br>
And let's change the color just slightly.<br>
So you get a sense of, you know, here's something different.<br>
Let's make it like a light blue, maybe purple.<br>
Anything different is all we need.<br>
And then let's go over here and try to include it.<br>
So we have a style sheet and check this out.<br>
If I select, type /static, I'm already getting some completion here.<br>
You get even better completion if you go over here and mark directory as a resource route.<br>
But it seemed like that worked pretty well for us.<br>
And if I run this, let's go and see what happens.<br>
Well, where did our green go?<br>
Green is gone because I took it out of the page.<br>
But why isn't it purple?<br>
Well, because this is not working.<br>
If I try to click, this is cached again.<br>
But a quick click this, not found, can't serve that.<br>
So the fix is easy, but not obvious.<br>
Let's go over here, told you we're doing more and more things in these various stages, which is why I broke it out.<br>
I want to go to the app, and I need to mount the static folder.<br>
And then what I need to do is pass along this thing called StaticFiles, and that comes out of Starlette.<br>
Not FastAPI, FastAPI is an API framework built upon Starlette, so a lot of times you'll see things like request, response things or this actually being driven by Starlette.<br>
We'll go in here, we're going to give the name of this as well.<br>
"static" like that.<br>
I think we gotta explicitly do it.<br>
These are all keyword arguments.<br>
And then, if that wasn't enough static, we're also going to say name="static" like that.<br>
All right, You would think this might do it, and it will get us part of the way there.<br>
Check this out.<br>
It says if you're going to use static files, we would like to be able to serve them asynchronously as fast as possible without blocking the rest of the requests.<br>
A really cool way to do async await file access is with this package called aiofiles.<br>
So, in order to use this feature, we have to install this extra library to work with it.<br>
So we're gonna come over here and say we're gonna use aiofiles.<br>
We would have basically done the same for Chameleon, but this package already depends upon Chameleon, so it solves that.<br>
So there's a bunch of things like this in FastAPI that are not used by default.<br>
But if you want to use them, you're gonna need to install additional dependencies.<br>
So yeah, and that's not misspelled.<br>
Let's try one more time.<br>
That should be all the hoops we got to jump through, create the static folder, mount the static directory with static files, and add and install the aiofiles package.<br>
Wuju!<br>
look at that, purple!<br>
Doesn't seem like a big deal.<br>
But that is our CSS file that we're now able to serve right out of here and for some reason, that keeps getting cashed, the view source.<br>
But there it is.<br>
There's our CSS file that we created, and we're serving out as a static file, so it doesn't look like much, but this means we can serve images, we can serve CSS, we can serve JavaScript.<br>
All the things that major websites have to do.
|
|
show
|
4:00 |
Now, let's just take stock of where we're going, what we're trying to build.<br>
Remember, this is our application we're building during this course, and here we have it running locally in a finished version.<br>
What you'll have at the end.<br>
But we're not there yet, are we?<br>
So this looks a lot like pypi.org.<br>
See,it's got some data in the database.<br>
We can go and see details about these packages.<br>
We can log in, we can log out.<br>
We can come over here and say /about to go the about page.<br>
So what I'd like to do is to bring in some of this design.<br>
This is not a web design course.<br>
This is just to teach you how to take the concepts of web development, including design and harness them with FastAPI and use that very powerful framework as the foundation instead of something like Flask or Django or Pyramid.<br>
So what I wanna do is, I want to copy this overall shared layout feel into our web application.<br>
Now, in our static folder we have site.css, which has the one silly thing there.<br>
We're gonna remove that and we're going to add in a completely different set of files here.<br>
So over here in our static, it's empty.<br>
But what we want to do is paste styles from somewhere else.<br>
I've got those copied from something I've already written.<br>
And over here, you could now see we've got a whole bunch of stuff.<br>
Look, we've got, say, our site CSS, which has a bunch of web design, our navigational design and so on.<br>
And we've got external CSS files like Bootstrap and jQuery and so on.<br>
jQuery is just JavaScript.<br>
We've also got some images.<br>
So that's just gonna be the general layout.<br>
You could go and study that if you want to mimic it.<br>
But the point is to show just how to use this and build a really cool application, right?<br>
So that is step one.<br>
The second step, in order to use that is to put our, go into this shared lay out here.<br>
Right now it's incredibly simple with just one CSS file that we actually deleted, but there's really not a lot going on here, so I'm gonna copy over the proper one, and let's take a look at what we have now over here.<br>
So we've got more style sheets, we've got Bootstrap, we've got the site, we've got the navigation.<br>
We have a few more slots to fill in if we want to add additional CSS, we may end up doing that.<br>
Over here, we've got a toolbar, a navigation bar that comes from bootstrap with things that look like the main PyPI site like "Donate" and "Help" and "login" and "register".<br>
Here's our main content as we talked about and then it's got a footer and some Javascript to make things go.<br>
Other than that, we've only dropped in the CSS.<br>
and the images, which change nothing.<br>
There might be something that's dependent upon what we pass over, but I don't think so.<br>
Let's give it a shot.<br>
See if this will work.<br>
Look at that!<br>
Now, that might not seem that impressive, right?<br>
But here's our content and here we go to our about.<br>
Here's our about, there's our home page.<br>
Notice we've got our common look and feel.<br>
Just like over here, you can see we've got our common look and feel, our navigation.<br>
Down the bottom We've got our footer.<br>
So just like that our site is ready to go and there's really nothing fancy besides I brought in just some CSS and some basic HTML and our goal is going to be to start writing the code that fills out these pieces as our login.<br>
This one's empty for the moment.<br>
Remember we didn't turn, do anything with what we're returning from there.<br>
We've got our register.<br>
But you see, those were actually, those links are starting to work.<br>
So very, very cool.<br>
We've got our overall look and feel, in the real web design, put together.<br>
Let's see how we did.<br>
Here's the real pypi.org.<br>
Roll that up, now we can scroll it up.<br>
It's just stuck there but look, pretty, pretty good, pretty good I think in terms of, you know, the navigation and such.<br>
So we got a ways to go, but we're making really good progress here.
|
|
show
|
5:43 |
So we saw that we were able to get a really solid looking overall layout.<br>
We have our navigation, we have our footer, all those kinds of things.<br>
And yet, when we look at this here, it still has this big fake section in the middle.<br>
Remember what we're aiming for.<br>
It's supposed to look like this, this cool search section, this info box here with three pieces of information, a list of packages.<br>
But what we're gonna do here is copy over the HTML and then write a little bit of code for first passing over fake data.<br>
And then when we get to the SQLAlchemy data-driven section, we're gonna have real data.<br>
So let's, just like before, go over here and drop in the real HTML.<br>
I'll talk you real quickly through it.<br>
There's not much to it.<br>
It uses the layout, the same, the same shared content section and so on.<br>
It has this little "hero" section that has that big header with the search text.<br>
We have the styles and the CSS we already included.<br>
We have that stats bar, that gray bar here and it has the package count.<br>
Instead of just showing it as like this, we're formatting the string to do digit grouping.<br>
So if it's 120,000, it's a 120 comma 000.<br>
We need to have a package_count, release_count and user_count.<br>
And then, we're gonna go over here and we're gonna do a loop through a list of recent packages.<br>
And for each package, we're gonna have the "id" and the "summary".<br>
So we've got to write a little bit of code for that to work because if we try to view this now, we'll just get some weirdo server error.<br>
So here we get this crash.<br>
So you saw we're getting an error over here, and the error is, and in Chameleon it's unfortunately super hard to track down the errors.<br>
You get all this junk here.<br>
Look at all this.<br>
In the middle, you could see that's the real problem.<br>
NameError package_count.<br>
Jinja, if you don't provide some kind of variable, it'll just come through as None and then whatever happens then.<br>
Chameleon makes sure everything is hanging together, it's good in that regard, but it's also a little annoying that you have to be exactly there.<br>
So let's go to our home and remember what we have to pass over.<br>
So let's just for now, add some fake data.<br>
And then, like I said, we're gonna be getting this from the database.<br>
You will be data-driven and we won't be writing this code here, but we do need a package count.<br>
Let's say there's 274,000 packages and there's a couple others right in there.<br>
We're gonna need a release_count, which will be two million ish like that and the user_count is gonna be, let's say there's 73,000 people who have created accounts here.<br>
We're gonna need a packages which is actually gonna be a list, and into each one of these, we're gonna need a little dictionary that has an id.<br>
So let's just say one.<br>
I think it's a "summary", it's what it is.<br>
We'll look at it in a sec.<br>
There's three things we need, I believe.<br>
And here we need the "summary" and the id.<br>
The id is like the actual package name.<br>
So this would be, let's say FastAPI, a great web framework.<br>
That uvicorn, favorite ASGI server.<br>
And httpx, alright.<br>
I think those three should do it.<br>
This is not misspelled, right?<br>
One more time.<br>
We have enough data.<br>
Is it gonna crash?<br>
No, it's not gonna crash.<br>
Oh my goodness, that looks fantastic!<br>
So here's our hero section.<br>
Here's those counts.<br>
Here is the number of projects, the number of releases.<br>
Notice the digit grouping?<br>
yes!<br>
Number of users, and here's hot off, the releases.<br>
Out three most recent projects or packages are FastAPI, a great web framework.<br>
Uvicorn your favorite ASGI server.<br>
And httpx, requests but for async world.<br>
Now these have links that if we click, don't go anywhere yet.<br>
We're still working on that section, but very cool, right?<br>
And maybe one thing we haven't really talked about and it's not necessarily super obvious the first time that you see it, would be, how do you do loops?<br>
Right, in Jinja, you create the percent you say "for thing in that", then you end the loop and so on.<br>
It's more implicit here.<br>
So this is the thing that's looping.<br>
Basically, this is, that will stop helping, it, this is "for p in packages", repeat that whole <div> section, including the part that has the "repeat" on it.<br>
So we're just cloning that <div> over and over, and each time through, we have this "p" variable that is one of the packages.<br>
So that's how we got that repeated list of projects right in the home page.<br>
Super cool, huh?<br>
So we've come a long ways from returning a string that comes out as JSON, converting that to an HTML response that just has embedded HTML.<br>
To now using a shared layout that brings in all the important pieces, including the navigation, a common look and feel, and then leveraging that overall CSS and designs theme brought in to bust out a cool little homepage that looks like this.<br>
We've used our template decorator.<br>
Go ahead and find the starting template, a Chameleon template, which then finds the shared layout through the template engine and so on.<br>
So I think we've come and built a really nice way to layer on proper web development on top of FastAPI.<br>
Of course, there's lots more to build.<br>
There's some cool data layers that we've got to talk about and things like that.<br>
Already, hopefully, you can see the potential for what we're building here.
|
|
|
38:37 |
|
show
|
1:44 |
Our web application has come really far.<br>
If you look at the design, it looks a lot like the PyPI that we're looking to build.<br>
We don't have our real data coming from the database yet.<br>
We're getting there soon, but if we had that in place, it would be quite close.<br>
However, there's a few things that I would like to touch on before we get there, that are going to help tremendously as our web application grows.<br>
So there's two core design patterns or concepts that we're going to go through and integrate into our PyPI clone during this chapter.<br>
One is called "View Models".<br>
Now "View Models" are curious when we're working with FastAPI They make perfect sense with Flask or Pyramid where there's no natural exchange mechanism between the HTML data and the view processing and sending that data back and forth between that template.<br>
But in FastAPI, we have Pydantic.<br>
So we're gonna talk about how "View Models" and Pydantic should coexist, when to use one, when do you use the other.<br>
Because you might think you can just use Pydantic models and to a degree, you can, but they're not nearly as advantageous as what we will get with working with view models, as we'll see.<br>
Also, we're gonna talk about services.<br>
These are not web services or APIs but just things that provide group functionality to our application.<br>
So there'll be a "package service" that lets us do all the database queries and processing and answer questions around packages.<br>
Similarly for users, if this was a real app, maybe we would have something that worked with email and it would figure out how do we resend a reset email?<br>
Or how do we load up templates and then pass that up and things like that.<br>
So the are two patterns we're gonna talk about: "View Models" and "Services".<br>
And we're also going to compare that with Pydantic as well.<br>
And you're going to see, we're gonna be in a really good place with our web app after this chapter.
|
|
show
|
3:41 |
Before we start writing code around this view model pattern, let's just take a high level conceptual look at where it fits in the whole web application process.<br>
So on the left we have what I imagine, some kind of HTML form where data is being exchanged with a browser.<br>
So the form represents like, say, the Chameleon or Jinja template.<br>
And it's got all the data that the users typing in and is going to be submitting back to the application.<br>
The application may also need to provide some data to that form.<br>
Like, for example, if there's a drop down of countries that you wanna pick Well, you've got to send over those countries to that form.<br>
Most likely you don't want to hard code that into the HTML.<br>
This view model, its job is going to be to manage that exchange.<br>
So on the left, we've got a browser with an HTML form that is driven by a template.<br>
On the right, we've got a big action method because, what happens here?<br>
when they submit this form for in this case, registering for our site, they're gonna put in their first name, their last name, their email address.<br>
Maybe they've gotta checkoff a re-captcha type of thing to make sure that they're not some kind of bot, and so on.<br>
Our website, our action method over here, our view method is going to actually need to look at that and say, well, did they supply a first name?<br>
Yes or no?<br>
Did they provide a last name?<br>
Yes or no?<br>
If they don't provide the last name, tell them they have to provide the last name.<br>
If they provided an email, make sure that it's actually a proper valid email, not just some random text.<br>
Did they provide a password?<br>
Is it secure enough?<br>
Like all of those things have to happen.<br>
And then we go and create the user.<br>
And at each step, if something goes wrong, we need to send back a message like, hey, this didn't work.<br>
And very importantly, we need to round trip that same data, if they typed in first name and last name and email.<br>
But email wasn't right, we want to reload that form with the same first name, the same last name and the same email and just say, here's what you typed, but please correct it.<br>
That is a really important part here as well.<br>
So the job of this view model is instead of writing all of that just in this one view method, this action method, which makes testing hard, which makes editing the code or adding features really hard.<br>
We're gonna break this action method into another part, using this view model.<br>
So it will have a small little action method.<br>
And the view model will be a class whose job is to understand all the data that the page needs, all the data that the user is supplying, how to round trip it and how to validate and report errors on it, as I just described.<br>
And then this action method is just going to say, hey, they submitted the form, please load it up and tell me if it's good.<br>
If it is, I'll create the user.<br>
If it's not good, we'll just send back that same data you have along with the error message.<br>
That's the idea of these view models.<br>
And like I said, it sounds like Pydantic would be a really nice choice here.<br>
But there's a couple of problems.<br>
Pydantic, when it runs into errors, it doesn't hang on to the data anyway and then let you send it back.<br>
It just crashes and says, hey, we tried to accept this data.<br>
It wasn't right.<br>
Sorry.<br>
And while that's super perfect for what you need in an API, it's not what you want for a web page.<br>
We'll see some concrete examples of this later when we actually write the registration method in the next chapter.<br>
But for now, let's just put it to the side that Pydantic models are perfect and such a cool library and way to work with data exchange with FastAPI for the API side.<br>
But they don't make as much sense when you're trying to make a web application with HTML templates.<br>
We'll come back to this in more detail, and we'll see exactly why that is, later.<br>
Here's what the view model pattern looks like and how we're going to use it to isolate the data exchange between the HTML template and some of our code and not mix that in all over the place.<br>
Make it easier to test that data exchange validation, and it will keep our view methods nice, clean and simple
|
|
show
|
8:44 |
Welcome to chapter five.<br>
Let's get started working on these two patterns view models and services So, as you can see here, I've made a copy of what we completed in chapter four and that's what we're starting with in chapter five.<br>
Let's go ahead and open it up and over here we're gonna focus on this view right here.<br>
Let's just run it and remind ourselves what it looks like.<br>
So it's this page here that shows a couple of things, and we're gonna add something else that's gonna be interesting that we weren't able to do easily before.<br>
Here, we've got the number of projects, the number of releases, the number of users and then the most recent projects.<br>
So what we're gonna do is we're gonna create a view model that provides this page, this template, that data.<br>
Now we're gonna start simple here because we're not exchanging data.<br>
We're not accepting data yet.<br>
Later, we're going to write a login and a register bit of functionality.<br>
This is gonna let us do many things, work with HTML forms and validation and all that.<br>
That's when you'll see these concepts of view models really shine.<br>
But I wanna, you know, make progress down that road until we can get to the user section in the coming chapters.<br>
What we're gonna do is we're gonna create a view model that just provides data to this page, and we're gonna have some services that work with projects and users.<br>
Let's go and create a "service"s folder.<br>
And like I said, we're probably gonna have to.<br>
I'll go ahead and just write it here, package_service.<br>
Again, this is not web service in the traditional sense of the API.<br>
This is a thing that bundles functionality and think of it as a layer between your actual data layer and the rest of your application.<br>
It knows how to do all the queries to SQLAlchemy.<br>
It knows how to interact with external APIs that you might need for, say, sendgrid or mailchimp or something like that, using either of those services.<br>
While the user service, here, the user service might know how to verify passwords with cryptography.<br>
In addition, it's not just data access layer, that's no what.<br>
It's why I'm not calling it that.<br>
OK, so we're gonna need a couple of things in order to do that, to make these useful.<br>
But let's go ahead and do our view models, now recall that we have some really cool naming conventions that allow me to say, well, I'm looking at this.<br>
Let's say I'm looking at this function, this view method.<br>
I want to know where's the template.<br>
I don't have to guess what it's called or like track it down.<br>
I go.<br>
OK, so it's home index.<br>
So it's home index.<br>
We're gonna follow exactly the same pattern for view models.<br>
That way, if you're in the view model, you know where the function is.<br>
That is the view function.<br>
You know where where the template is.<br>
It's really nice and organized.<br>
So I'm gonna go, create a directory called viewmodels.<br>
And in there we're gonna make a directory called home, and at home, we're really just gonna probably need one view model.<br>
We're also gonna need to have a common base class.<br>
I'm gonna create something called shared, and I'll put, I'm gonna calle it viewmodel_base in here.<br>
Maybe just a few models.<br>
Here we go, and we'll have a class.<br>
Now, it might seem silly like, why do I need this base class?<br>
What is its job to be?<br>
There's a couple of things that we want to make sure every single page has all the time.<br>
For example, when we do user management stuff, we wanna have it be the job of these view models to tell you what user is logged in and potentially provide you with that user and so on.<br>
If you're in a form, they always are gonna be able to provide an error message if something goes wrong with the form.<br>
So there's a couple of things like that that are gonna be part of this base model.<br>
The other one is, we need ways to easily provide the fields of the class as a dictionary.<br>
So those are the two rules here.<br>
So a ViewModelBase, let's call it.<br>
And down here its gonna have a couple of things so we'll have an error, which is an Optional, we're gonna need some typing here, string, which is None.<br>
And let's have ah user_id, which is an Optional integer, which is also None.<br>
So those were the two core shared pieces of data.<br>
We're probably gonna pass around some more.<br>
It's often helpful to have access to the request that comes in as you'll see for, like, cookie management and stuff like that.<br>
So let's go and add that in here as well, and PyCharm will gladly add that.<br>
Now wouldn't it be cool if it typed that for me?<br>
OK, so it'll have this stuff and then, in order to pass these things back we're just gonna take advantage of the fact that when you do this, what it's really doing is putting an entry in the class dictionary.<br>
So we'll say def to_dict, gonna return a dictionary.<br>
And what's gonna be in here, we're gonna do is just return self.__dict__.<br>
So this is looking good.<br>
This is gonna be something common to all of our view models.<br>
We're probably never going to touch it again.<br>
And then, now let's go.<br>
Remember, we are in the index, and in the home I'll say, indexviewmodel.<br>
Add a class IndexViewModel like that.<br>
I'ts donna derive from our base class, which we import.<br>
Thank you, PyCharm.<br>
And it's going to need, yes, it'll need a constructor.<br>
Alright And PyCharm says there's one that requires something passed in so well let PyCharm write all that code for us.<br>
Thank you, thank you.<br>
Super cool.<br>
So let's go back.<br>
Now that we've got our structure in place, Let's go back up here to our home view and say, What were we doing before?<br>
we were coming up with all these elements, this dictionary that has a package_count, a release_count, user_count and a list of packages.<br>
I'm gonna copy this over.<br>
And this will give us a hint on what we're going to need in our indexviewmodel.<br>
Over here tells you that we're probably gonna need a self.package_count, which is an integer, and I'm just gonna put None, I'll put zero for a minute.<br>
We're gonna come up with a better answer in a moment, and we need a release, release_count and a user_count.<br>
And then it is the order here we're gonna need packages.<br>
Which is gonna be a list of something.<br>
We don't know what goes in the list yet because we don't have a class for it, we will in a minute.<br>
And we'll just say like that.<br>
OK, so let me go and comment this out and see if we can now swap this out to be our view model.<br>
And I'll just put 1, 2 and 3.<br>
So we have some little big data, it's just so you can tell actually, something is flowing from one place to the other.<br>
So instead of this.<br>
What we're gonna do is we're gonna say vm for view model.<br>
It's one of these.<br>
We've gotta, of course, import that as above.<br>
And then we have to pass in the request.<br>
PyCharm'll help us get that close.<br>
close.<br>
We've gotta type that in.<br>
FastAPI uses the type hint here to know that what you want to pass in is the request.<br>
So this is all we have to do, and then it'll start passing it over.<br>
And then again, if we don't pass anything, watch what happens.<br>
We get a crash and the crash is NameError somewhere.<br>
The NameError is right here.<br>
package_count.<br>
You're supposed to give me a package_count, and you didn't.<br>
So let's provide the data from here, which will be to_dict.<br>
It was basically like before.<br>
It's gonna build up that dictionary like we saw.<br>
Not as much data, but close.<br>
Yes!<br>
Look at that 3, 1, 2.<br>
Maybe we could've changed the order to go 1, 2, 3 but still cool, right?<br>
So it's provided this information.<br>
It's gonna get it from somewhere else in a minute, but it's provided this information, and it's given us an empty list of new releases.<br>
We're gonna need to get them from somewhere again.<br>
Pretty awesome, right?<br>
So this is how we're going to pass data over to our template.<br>
Now let's go back and look real quick.<br>
Suppose that we want to make sure that the request is not None.<br>
We want to make sure that you know some data that we're pulling back is valid here or if we pass in more things like we're passing a form that the form contains the elements that we expect.<br>
It's really, really easy to write a unit test against that class right there and do that validation without doing any web test infrastructure, which is always kind of a pain around unit testing web applications.<br>
So it allows you to really carefully test the exchange between the template and this thing You could even actually use the template engine to directly try to render a test instance of one of these things.<br>
So that's pretty cool.<br>
It's also going to keep this method extremely simple.<br>
For real ones, it's gonna be slightly more complicated than that but not a lot.
|
|
show
|
3:41 |
Well, we had our little random fake data here, but let's move over to using our services to provide data here.<br>
So what I wanna do is, I want to go to the package_service.<br>
And I wanna have it answer a couple questions.<br>
And I can hit Ctrl + space two times and PyCharm will automatically import it up there.<br>
Awesome, and I wanna have just a couple of functions that'll tell me how many releases for packages are there and how many packages are there.<br>
So we'll say release_count something like that and we'll do right there package_count.<br>
Let's spell that right: package_count.<br>
Now, those don't exist yet, as you can see and we'll have user_service.<br>
Again double Ctrl + space puts it at the top, and what we want to do is say, user_count.<br>
And down here, let's say package_service.latest_releases() and let's pass in limit=5.<br>
We don't want to get all 200,000, we just want the five latest releases.<br>
This is what we want our services so far to look like, to provide data over here.<br>
We can have PyCharm, write it for us, and we can say this returns an int, which is great.<br>
We're gonna need that again.<br>
So I'll just copy it.<br>
And down here user_count and create that function there, returns an int.<br>
And this one is going to return an int, this is where we're going to write our SQLAlchemy queries in a minute, when we get to there.<br>
But for now, eventually it's gonna be a list of package classes but we don't have those yet.<br>
This is gonna be an int, and the default will be five.<br>
Now, here's where we would go and do a count against a query for the release table.<br>
We do a count against query for package table or we do an order by and then a limit over on.<br>
I said latest_releases.<br>
We want that to be latest_packages.<br>
Sorry about that.<br>
We'll do a some kind of query that orders by the release dates and then does some kind of limit on that.<br>
That's where we're going, when we get to the SQLAlchemy section.<br>
For now, let's just go and put these numbers back in there.<br>
So, which one was that?<br>
That was package_count.<br>
So when you get package count, we're returning that number.<br>
When we get to the release_count, it's that two million.<br>
Again, this is just fake for now, just to get the layers in place.<br>
I wanna keep going from there, right?<br>
So user_count is going to be over there, clean this whole thing up.<br>
Perfect.<br>
And lastly, here's what we're gonna pass back for the packages and let's go and use that limit.<br>
Obviously, we're gonna go against the database, but because slicing is so easy, we can say go from the first up to however many they pass in.<br>
That way, if we say two or whatever, we'll be good, let's go and run this one more time and make sure our app is hanging together.<br>
Starts, that's a good sign.<br>
There you go.<br>
Look, now we're back to what we started with.<br>
Great refactoring, huh?<br>
So here we've got our projects, our releases, our users and here are our latest packages again.<br>
The difference, though, is this time, around the view method.<br>
Again, this one's pretty simple but when we do like registration stuff, there's still gonna be a decent amount of things happening here.<br>
So this lets us isolate that data exchange, This class has one job.<br>
Its job is to know what this one temple needs.<br>
It needs package_count release_count, user_count and so on.<br>
Its job is to go get that data and then provide it to it.<br>
And if it were some kind of form, accept that data back, validate it, convert it, and so on.<br>
Cool.<br>
So now we've got our app converted over to use view models.
|
|
show
|
3:08 |
We saw our one complex page so far, our index view, with the page that has the counts of the packages and the latest ones and so on got converted to being, to using view models.<br>
But the others, there's something that we could also do as well, and we'll see why.<br>
Maybe I'll write the view model here for this one, but we're gonna leave it off for just a second.<br>
In fact, we could just use the base one on that one.<br>
But if we look over our account view, which is right here, notice that there's a bunch of stuff going on and we're not actually using them yet So just to, while we're on the topic, let's just go ahead and put some placeholders in place here.<br>
So I'll take you through the process again.<br>
We're in the account view module.<br>
So what we need to do is go over here and create a directory called account, and we have an index, we have a register, we have a login and so on.<br>
Let's go knock those out real quick.<br>
So, IndexViewModel.<br>
I mean, we call it just AccountViewModel because it's slash account.<br>
You can decide what you want to call it.<br>
I guess we'll go and call it that.<br>
So this'll be a class.<br>
I don't like the name.<br>
Hold on.<br>
We'll fix that, let's go over here and rename it.<br>
What I would prefer is having account, maybe like this, it'll make it easy to read viewmodel.<br>
OK, pass is fine.<br>
We're just gonna leave these alone for a minute.<br>
Easiest way to create more is to just copy paste.<br>
So let's have a register_viewmodel.<br>
Make sure we change the name.<br>
And we're gonna have a login_viewmodel as well.<br>
We don't really need a logout_viewmodel because that's just gonna make a change and send them somewhere else.<br>
So LoginViewModel, and then we can just start using those here and say vm equals this AccountViewModel, that.<br>
It's going to need the request.<br>
Which we want to have it at the perimeter.<br>
Thank you, like this.<br>
And then again, here what we do is just say, return the dictionary of this thing.<br>
I'll just copy that because it's really similar, register and login.<br>
It's this view model here that you're going to see that actually is where a lot of the magic of these view models come from.<br>
So we got that in place and then I think that's pretty much it, you'll see over here that we could do this, just equals ViewModelBase like this and pass over the request like that.<br>
But I'm going to leave this out for just a second.<br>
Because I wanna show you one of the benefits that we're gonna get from using view models and sort of run into an issue here.<br>
And then we'll come back and just say, well, look, if we add this view model, everything works great.<br>
All right, so that was just a little bit of housekeeping to get everything set so we can start writing the rest of our application.
|
|
show
|
1:08 |
Let's see how far we've come with our website here.<br>
So we go over, we got our login and register.<br>
Those don't actually have the right return value yet, but that's OK.<br>
We'll come back to that in a second.<br>
But over here we've got these packages and we've got lists linked to them, right?<br>
So we go /project/fastapi, /project/uvicorn I personally would probably choose like /package or /packages/ that.<br>
But if you go to the real pypi.org this is the url structure they use, so we're just gonna stick with that to be as close to what they are creating as well.<br>
Now, if you were to go to the real PyPI and let's just look at FastAPI real quick, and go in here and you scroll down you notice there's quite a bit of stuff.<br>
There's this sort of main banner section with these different installs.<br>
What the release was, whether or not it's the latest version, stuff about the history and the versions and the forks and the authors, then a big description here in the middle and even if you go to the release history, go back to a different line, you can see it shows you different install statements, information about whether or not it's the latest version and so on.<br>
That is what we're going to build.
|
|
show
|
3:44 |
Now I'm going to start again from existing HTML because, like I said, this is not a web design course, this is how do I do websites in FastAPI.<br>
So we're gonna just look at some Chameleon and then go from there.<br>
This is what it's gonna look like, we've got our content details.<br>
Again we're using our shared layout, as we will with basically everything.<br>
Here's our little hero section pip install such and such.<br>
And if it's not the latest version, we're gonna do an equal equal the release version that's selected.<br>
Stuff like that.<br>
Here's the summaries, the stuff along the side, and when you download etcetera, etcetera.<br>
So there's a lot of things that we're going to need.<br>
For example, we need a package, which has a license, a package which has a description.<br>
We're gonna need whether or not it's the latest release.<br>
Let's see over here.<br>
So what we're gonna do is we're just gonna go through and try to get this to run.<br>
We're gonna miss a few things I'm sure.<br>
We'll then go back and update that as well.<br>
I'll just copy this for a second, that will give us the simplest path to do that.<br>
Got a couple of things to import Starlette's Request, fastapi_chameleon's template.<br>
And here let's just call this details, DetailsViewModel and let's call this details.<br>
Remember, the URL is going to be slash project, wish it was packages and then it's some value that goes here.<br>
It was a package name.<br>
So the way we do that in FastAPI is like this, we put it in here and we give it a type.<br>
This one is just a simple string.<br>
OK, so that means this is a required element in the URL.<br>
There's no defaults and it's going to be a string.<br>
And because of the somewhat unusual naming structure here, we need to set the template file explicitly.<br>
We can't just go with the standard convention.<br>
So what is it?<br>
It's packages/details.pt Here we go.<br>
Now we don't have this DetailsViewModel.<br>
Again it's gonna have the same structure.<br>
So in the viewmodels like before, the easiest thing to do is probably make a copy, make a new directory called packages, and in there we're gonna make a new Python file called details_viewmodel.<br>
Go do it yourself, copy this one over.<br>
So this is gonna be DetailsViewModel, and it's not gonna have any of this.<br>
A little placeholder.<br>
OK, so this is going to be our basic structure and import that.<br>
We also need to pass over the package_name, right.<br>
So this is part of the URL.<br>
In order to get it, we're gonna pass it on, so I'll copy this and put it right there.<br>
Perfect.<br>
We want to make sure they passed over a valid package name, and that is not empty.<br>
The routing will probably do that for us.<br>
But what we can do is we actually want to make sure that not only is this a valid name, but if we do a query against the database, we get an actual package back.<br>
What we're gonna do is gonna say self.package is going to be package_service.<br>
And I'm gonna do a query, let's say get_package_by_id() And package_name is what we're using here.<br>
Then, if we don't have any value for the package, we're gonna want to make sure that we return some kind of 404 and so on.<br>
So we'll say, somewhere we'll say something like this, not that, return.<br>
OK, so we've got to write this function, and right now this is gonna be a string.
|
|
show
|
7:57 |
I think it's probably about time to create a class for our package.<br>
Eventually, this is gonna to come out of the database using SQLAlchemy.<br>
But for the moment, let's just get sort of started down.<br>
Cause you saw when you looked over here and we reviewed this.<br>
There's a lot of stuff, like a package has an id, and it has a summary and there's one more thing down in a home page and so on.<br>
So what we want to do is take all those things and put them into a class that ultimately we're gonna map into the database.<br>
So let's make a new folder.<br>
I'll call this data, something like that.<br>
Alright, ultimately this.<br>
Like I said, it's gonna be a SQLAlchemy model, along with many other things.<br>
But for now, it's just gonna be a simple class.<br>
For now, when we create it, we want to just pass over this name.<br>
Here we go, we won't add any validation or anything like that.<br>
We just need enough going on here so that this works.<br>
So I need to pass over the package name, which also turns out to be the id, gonna use that in the database for the id.<br>
And this now, we can say is going to return an Optional[Package].<br>
OK, so we're gonna just return an empty one for now.<br>
Just return None, were gonna get this working in a second, but let's go and finish filling this out so that it will work with our details.<br>
Let's just go through and see what we need.<br>
We need an id, we need a summary.<br>
Here's the id again for just the URL, home_page.<br>
And I need a license like, is it MIT, BSD or whatever.<br>
author_name, here's a bunch of maintainers.<br>
So it's going to need maintainers.<br>
I'll just make that an empty list, license, I think we've got that already, yes license again.<br>
Description and that's gonna be it I think, perfect.<br>
The short description and a long description.<br>
OK, so let's say we also need to pass over a couple more things here I need a summary, description.<br>
I'll just knock these out and we'll assign them.<br>
All right, we've got our package created.<br>
Now, there's one other thing we're gonna need to do to make this page work.<br>
Looking here, a little bit simpler, but we do have more information.<br>
We have a latest_release, which has a created_date.<br>
I think that that might be all that the release has.<br>
But for the same reason, let's go and create a class here, call it release.<br>
We'll add this latest_release over here.<br>
This is a datetime.datetime, perfect.<br>
And it's gonna have other things as well.<br>
Let's go ahead and say it has a version, it's a string like "1.0.0" or so on.<br>
Great, so that will be what we can add over there as well.<br>
Now back to our view model.<br>
And let's just make sure that we're passing enough details over.<br>
Again, there's a lot going on here so we'll have self, and we have the latest_release.<br>
I will just call this.<br>
Actually, this was created_date, I believe.<br>
The variable was called latest_release.<br>
Perfect.<br>
It's gonna be a release.<br>
We'll just make up some fake data for a minute.<br>
What do we need?<br>
let's say "1.2.0" and this will be datetime.datetime.now and for our get_latest_package_by_id, let's put it in here and just have it create package, and put some info into it.<br>
So what we're gonna need?<br>
we're gonna need package_name, that's what we're passing over.<br>
Summary will be "This is the summary", "Full details here!".<br>
And for the home page let's just put, passing over FastAPI, obviously this is hard coding it, but we got a whole bunch of data coming along soon.<br>
The author will be Sebastian.<br>
Here we go.<br>
Sebastian Ramirez, and the maintainers can be empty.<br>
All right, that's gonna be our package, and we'll just return that.<br>
Again, this is gonna come from the database, but for now, we're just gonna return some fake data.<br>
And while we're doing this, let's also, one more thing, let's have this package_service.get_latest_release_for_package and let PyCharm add that final function, which is going to get a string and it's gonna return this.<br>
It's an Optional[Release].<br>
Maybe they're asking for the release of a package that doesn't exist or the package doesn't have a release yet.<br>
Something like that.<br>
Here we go.<br>
I think we might be good.<br>
Let's go and run this and see what happens.<br>
There's still gonna be one or two little pieces.<br>
We need to put in.<br>
The moment of truth.<br>
What happens if I click this?<br>
Crash!<br>
I'm sure there's a NameError somewhere.<br>
Let's go find it.<br>
And latest_version.<br>
So latest_version we'll be.<br>
There's a few little things like this that we gotta figure out, let's just go "0.0.0" and if there is a package and there's a release, then we'll have to leave.<br>
But if there is one, latest_version, latest_release.version, I'm gonna say that.<br>
For the moment, let's just say self.is_latest is True.<br>
We're going to come back and we'll be able to check what release they've asked for and whether or not that is the latest.<br>
Down below, Yes!<br>
we've done it.<br>
We've definitely done it.<br>
OK, so there's a couple of things we're gonna need to do to make this work a little bit better, but we're quite close.<br>
So here, like if we go to the project's Homepage.<br>
Notice, it takes us to FastAPI.<br>
Here's the description, here's FastAPI for the name.<br>
The license is MIT, the author is Sebastian Ramirez.<br>
Here's a little summary.<br>
One thing that's missing though, is there's an extra CSS file for this page.<br>
Come over here to static and you see this package?<br>
We need to use this.<br>
Well, where does it go?<br>
Remember our layouts and our shared look, we had this define-slot additional-css that goes right there.<br>
This is that structured way.<br>
This is the location where additional CSS goes.<br>
Not before site, after site, for example.<br>
So we're just gonna go down to our details, and if we're doing any of those things, we're not.<br>
Let's close this up, a little easier to read.<br>
So what we do is we just put a <div> and we say fill-slot and we can omit the tag.<br>
And then what do we want to put here?<br>
We just put a link to /static/css/package.css.<br>
Format it a little bit, run it again.<br>
And tada!<br>
There we go, We have all the final designs brought in.<br>
If we do a View Page Source, notice the indentation.<br>
There's not much we can do about, but right in the location we expected, there is now a package.css included.<br>
I love that, and layout that we got working here.<br>
So this is really, really nice.<br>
Now we're using fake data.<br>
Again, we're about to get to the database, but this is really nice.<br>
What we've got going on here.<br>
We've got our home page, so let's go back home, we got our list here.<br>
Click on uvicorn, and of course, it's not using any of that data other than just that little bit right there.<br>
Still, pretty cool, right?<br>
About close to a functioning website.
|
|
show
|
4:50 |
Let's go look at our views one more time.<br>
We're just about there.<br>
We've got our account stuff going on here.<br>
Account views with their various view models, and we've got the home view with it set up.<br>
And the package view going on here.<br>
These are all good, but if we look here, there's this one we left this TODO.<br>
Remember that?<br>
Let's see one more thing to do with these view models that is really, really valuable.<br>
So one of the things that you often want to do is provide some feature, some functionality, to every single part of your application.<br>
Every template.<br>
It's just gonna be common across there.<br>
So let's look at a really straightforward example that's over here in shared.<br>
I've got this login and register.<br>
Well, guess what?<br>
What if I'm logged in?<br>
Do I still want to see login and register?<br>
No, I want to see a link that says your account and a button that says logout.<br>
Wouldn't that be nice?<br>
So how do we do that?<br>
Well, it's really super simple.<br>
Over here in Chameleon, you just say tal:condition, and then what?<br>
Like, is there a user_id?<br>
Let's say that that's gonna be something we can get really cheaply because it's gonna come from the cookies, which is just a dictionary, not from the database, which maybe would be more work.<br>
So instead of testing for a user, we're just gonna test for the user_id.<br>
Maybe a better way would be, say is_logged_in, are logged in or has_account or something like that, right?<br>
Make it a little more abstract.<br>
That would be nice to have.<br>
We'll say that we don't want.<br>
It's gonna, It's not the case.<br>
And if it is the case, then we want to say, change these around a little bit.<br>
So if you are logged in, we wanted it to go to just /account, and this is gonna say "Account" and this is gonna be/account/logout and down here it'll say Logout.<br>
That's great, right?<br>
Let's go and run it.<br>
Mmm, not as great as I was hoping.<br>
Probably not as great as you were hoping either.<br>
What is the problem?<br>
without looking.<br>
It's NameError: is_logged_in not found.<br>
Right there.<br>
It doesn't know what the heck that is.<br>
So what we do is, we go to our base view model and it always pulls this and sets this.<br>
I'll say self.is_logged_in = False.<br>
And then maybe a comment, right.<br>
We'll set that from a cookie.<br>
And it'll be really straightforward.<br>
So what's gonna happen is, the view model, anything that is either this or derives from this is going to have that information.<br>
So every single view now should have that info there.<br>
And what is our problem?<br>
Oh, I said case, didn't mean case.<br>
I meant condition.<br>
Somehow I got that, condition.<br>
Sorry about that.<br>
I'm sure you noticed that and you were like: what is going on here?<br>
Condition, I said condition, but autocomplete gave me case.<br>
There we go.<br>
Again, perfect.<br>
Now, right now, we said there's nobody logged in and notice if I go to any page on the site.<br>
Always working, go over here to our ViewModelBase and just say True.<br>
And obviously you wouldn't just hack that and say, of course, you're always logged in.<br>
What you would do is get that from user.<br>
But check it out.<br>
Now we have Account and Logout.<br>
Have it over on the home page, we have it on this page and so on.<br>
But if we don't use the view models everywhere, remember our about page?<br>
Crash!<br>
Again, all of a sudden, what is going on with our site?<br>
Well, here's that NameError that I was telling you about.<br>
In this particular view, that information our shared layout needed, didn't get what it was expecting.<br>
So it crashed, right?<br>
So these view models provide a really nice way to send that information to everything as long as everywhere you use it.<br>
And if you don't need any details, we can just say return the to_dict() or just the base, right?<br>
We didn't have to go create, like, an AboutViewModel here, but by using it everywhere.<br>
This is working great.<br>
There's other ways to do this, the view models provide a really nice mechanism.<br>
And if you're using them in most places, just use them everywhere and you'll be good.<br>
So back here that works, this one works, everything works.<br>
Great, just one nice chance to show how this ViewModelBase provides common data and common functionality for the entire template engine.<br>
Well that's it for view models.<br>
I hope you found this way of partitioning our logic and data exchange into view models and services really nice way to keep things like our actual view methods really clean.<br>
Create some testable little classes that we can test in isolation and provide common functionality to the entire template engine.
|
|
|
39:47 |
|
show
|
0:49 |
We've added some different links, depending on whether or not a user was theoretically logged into our website.<br>
It's time to actually greet users, let them log in, let them register, using HTML forms for our site.<br>
There's two main takeaways from this chapter.<br>
One, how do we deal with having users, who need to have accounts that are stored securely in our database?<br>
And, how do we do HTML forms we can get and post back for FastAPI with templates.<br>
We're gonna combine these things by creating the login and the registration features for our users in this chapter.<br>
We're gonna learn a lot about how to work with these forms and we're gonna use view models as the way to do some of that exchange.<br>
I think you're gonna see this works really, really well with FastAPI with it's native understanding and embracing of HTTP verbs.
|
|
show
|
1:46 |
Let's start off this whole chapter by creating a basic user class.<br>
And then we can use that for our form, our validation, our logging in.<br>
All that kind of stuff.<br>
So you can notice again that I have created a copy of what we finished for chapter five.<br>
Now that's where we're starting for chapter six.<br>
Over here in our data section, we have package, we have release.<br>
Let's add one more class, for the user.<br>
And let's call it class User, and it's gonna have a couple of things.<br>
It's gonna have a name.<br>
So, it has name, it'll be a string.<br>
It's gonna have an email.<br>
It's tempting to say it has a password.<br>
You never, ever wanna store the password.<br>
You wanna store a hash of the password and we're gonna use a really cool library for this.<br>
Another thing that's nice to know when people sign up is: when did they create their account?<br>
So let's go over here and say a created_date, and I'll just put that as None for the moment.<br>
There's two more things, we're gonna have a profile_image_url, which is gonna just be some kind of string that we pass over, and we'll have last_login, which is a datetime, perfect.<br>
So this is our basic user class that we're gonna work with.<br>
And this is optional, but we'll come and make it work when we really do our actual thing with it.<br>
Let's give it a few pieces of information.<br>
Let's say we're gonna pass over the name, the email and the hashed_password.<br>
Those will do it for us for now.<br>
Perfect.<br>
So we're gonna go and create this, add couple of users with our form.<br>
Let them login through our user service, things like that.<br>
Here's our basic User class.<br>
Remember, what we're going to do is convert this to a SQLAlchemy ORM model with basically these columns.<br>
But until we get to the database in the next chapter, we're just gonna work with it in memory.
|
|
show
|
3:02 |
Before we go about writing our login form or a register form or any HTML form.<br>
Let's talk about a pattern that is super common on web applications, and we're gonna use it here in FastAPI.<br>
It's the GET > POST > Redirect pattern, and for sites that are designed well and you've had good experiences with, I'm sure they're using this pattern over and over again.<br>
Let's talk about it.<br>
So we've got our server, our server stores it's data in a database, and we've got you sitting on your laptop working with your web browser.<br>
Let's suppose you want to go to the site that's hosted on the server, you want to create an account, you want to register for the site.<br>
How do you do that?<br>
You visit a page and it shows you an empty HTML form.<br>
The way you do that is you do an HTTP GET request against /account/register and an empty form comes and says, great you'd like to register?<br>
what's your name?<br>
what's your email?<br>
what's your password?<br>
They always make you confirm your password, or sometimes even your email both of those are annoying to me.<br>
I don't really like it that they do it.<br>
But, you know, that's how these things go, right?<br>
You have one of these, these types of forms that gets shown here.<br>
Then, you edit it locally and you want to say I filled it out, save it on the server.<br>
What you're going to do is an HTTP POST, often back to the exact same URL.<br>
That's gonna take the form, post it back to the server.<br>
Server's gonna look at that and say, well, now you're submitting the form instead of wanting to just see it because it's a POST and not a GET and it has data, it's not empty.<br>
If it's good, you get to carry on.<br>
If there's a mistake, the form, that POST will just reload the page and say there's some kind of mistake, you know, like that account already exists, you can't use that email address or whatever.<br>
But let's assume that it worked out well.<br>
What it's gonna do is save some data to the database, and then it's going to send a 302 redirect like you submitted the form but now we want you to go over to /account.<br>
The last thing you want is to just stay on that registration page and say OK, now go find somewhere to go.<br>
No, you take them where you want to go.<br>
So in this case, they've created an account, let's take them to their account, or maybe to their onboarding for whatever this website is, they'll GET > POST > Redirect.<br>
Super, super common pattern on many, many websites.<br>
And that's how we're gonna construct, how we're gonna put together our user login as well as our user registration.<br>
So we're gonna have one view that handles the GET to register, the one that just shows the form.<br>
The other one is gonna handle the POST which actually validates the data and creates the accounts.<br>
Now, if you look around, you'll see that this is not a pattern that I just made up.<br>
It's also something that gets used in a lot of places.<br>
Over on Wikipedia, they call it the Post/Redirect/Get To me, and I don't think so, I think of it as GET > POST > Redirect.<br>
You start with an empty form, you submit it, you go somewhere else when you're done.<br>
Here, it kind of starts like halfway through that process.<br>
Like I guess you're posting something you magically acquired, right?<br>
So anyway, they call it Post/Redirect/Get.<br>
You'll see it under several names, choose the one that you think that makes the most sense.<br>
So common pattern.<br>
Anytime you're working with server side HTML forms, which we are, and we're gonna do that for our account management.
|
|
show
|
5:36 |
So far, our login and register haven't done anything.<br>
Remember, if we look over in the views, they just return empty stuff and they were actually crashing because they didn't know a template that went with them.<br>
So we're gonna address that in just a second.<br>
But let me go and copy over some HTML, just like before.<br>
I'm just gonna use some standard HTML that's already here.<br>
So, for example, when you just go to your account it says "Welcome to your account", whatever your user is dot name and when you go register it's gonna provide you a form that had, lets you type in your name your email, your password and if there's an error, it'll show you what that is.<br>
OK, so this is a standard HTML form that we're gonna use, and we're just going to go here and we'll do.<br>
an @template(), let's see, we've got account index, got account index.<br>
I think we could just throw the template on like that and like that.<br>
OK, so this is almost ready to work.<br>
But if you go and actually look at, say the index, it has a user which has a name and if we go to our view model, it's not going to provide that data.<br>
So what we're gonna get is that there's going to be a NameError, so let's go quickly provide a user.<br>
So this user will probably come from the cookie that we get, we'll get an id.<br>
And if we have an id out of the cookies, then we'll be able to pull them out of the database.<br>
But for now, we're just gonna say the user is going to be a new user, which we're gonna hack together in here.<br>
And it takes the name, so I'll just put Michael and michael@talkpython.fm, and the hashed password is, whatever it is.<br>
And we also need to call the base, like that.<br>
OK, so this, let's double check that our user has a dot name.<br>
They do, so this should work.<br>
All right, now, let's go and give it a try.<br>
We need a way to log into here and indicate whether or not we're logged in But it looks like that is working in terms of the HTML.<br>
That's cool.<br>
And let's go see about doing something similar for register.<br>
If we look at register, we're gonna have to provide some information over to it name, email, password.<br>
We go to our view model and do the same thing.<br>
def __init__, add the super() call like that.<br>
And we're just gonna have a couple of strings.<br>
So we'll have email, the string, an optional string because maybe somehow it didn't get submitted.<br>
Gonna start out None, and as a minute, at a minimum, we have name, email, password, and then the error is gonna come from the base class.<br>
So there's nothing special we have to do there, And let's see if we have everything we need.<br>
Name, email, password, error.<br>
I think we do.<br>
Notice that we're also pulling in some extra CSS to make this look good.<br>
We go back and try to register.<br>
Tada!<br>
Look at that.<br>
We also have age in years, we don't really need to use that.<br>
But it was there, just for some, we're gonna use it just to show some of the client side validation in just a little bit.<br>
All right.<br>
So we've got our name, we've got our email address and we got a password.<br>
What happens if I fill this out?<br>
I really love the password "a" So that's nice.<br>
I'm gonna go with that.<br>
And my age in years is 18.<br>
Click register, "Method Not Allowed".<br>
What is that?<br>
Well, this is our POST, remember when we went to it like this, that's doing the GET.<br>
But when we pressed that button, it's submitting the form, which is a POST.<br>
Now if we go back and look at PyCharm here, at the view.<br>
What do we say?<br>
well we say, well we can handle an HTTP GET right here to this URL.<br>
But where are we handling the HTML, the HTTP POST?<br>
we're not.<br>
Remember the GET > POST > Redirect pattern.<br>
So that's what we need, to add here.<br>
When we do a GET, we're just gonna pass along the empty email, empty name and so on.<br>
And then when we submit it, we want to do something different.<br>
So we're gonna have a totally different function if you're doing something like this.<br>
Like if request dot method is GET do something, else do, If it's POST, do something else.<br>
No, that's doing it wrong.<br>
You want separate functions because their job is entirely different.<br>
One is to create the form.<br>
Another is to create the user and then send them on their way.<br>
So this one is going to be our POST.<br>
Now let's just do a print really quick.<br>
We'll say print "GET REGISTER", and "POST REGISTER" just to see that these are happening because you won't really be able to tell until we do a little more work.<br>
So let's just make sure.<br>
Here we've got that form.<br>
We're doing our GET register.<br>
I'm gonna fill it out.<br>
Register.<br>
Reloaded the form because we haven't changed anything about where it goes, but notice that's the POST.<br>
So that's the GET > POST > Redirect pattern.<br>
And what we're gonna do is we're gonna need to do a little bit of work This is where it's gonna get interesting.<br>
A little bit of work to grab the data from FastAPI, to pass it over to the view model, to validate it, to make sure everything is looking the way it should be.<br>
And then, if everything is good, we're gonna actually send them over to their, /account details view up here.<br>
If it doesn't work, we want to just tell them it didn't work, here's why.<br>
Please try again.<br>
But a lot of work to do here, but we've implemented to GET, POST almost Redirect pattern.<br>
We're going to do the Redirect later, but here's how we handle the GET and POST submission of the form.
|
|
show
|
7:09 |
So we've got the GET > POST part of our GET > POST > Redirect pattern in place, and we no longer need those little prints to see it in action.<br>
Now, there is another thing that we need to do here.<br>
We need to work with the data that's being passed back into this form here.<br>
So what's happening in this part is we're creating the blank data to pass along to just create the empty field.<br>
But we need to accept the data that's being passed over to it from this template through FastAPI.<br>
So give it something, say load from this form.<br>
In fact, we don't have to even pass this over, we're gonna be able to get this from the request itself.<br>
So let's go ahead and add this function.<br>
Now, why is there this separate thing?<br>
Why is this extra one?<br>
Because if we just grab the form right here and and check, it's going to be empty for the name in the GET version, but it's not gonna be empty in the POST version.<br>
It needs to be empty in the GET, it cannot be empty in the POST, right?<br>
So we wanna have different behavior and this, this allows us to say, like, OK, here's the kind of get it loaded up, GET version.<br>
And here's what we do in the POST.<br>
So this is gonna be interesting.<br>
What we need to do is going to say the form which is gonna effectively be like a dictionary is gonna go to the request, and we just say form() like this.<br>
Seems good, right?<br>
But if we say form dot notice the stuff that PyCharm is showing us coroutine, coroutine, coroutine, coroutine, awaitable, awaitable object.<br>
So what this is, when we do this form read here, there's a couple of things that are happening, and one is that this is an asynchronous read.<br>
It could be some kind of blocking thing, like maybe it requires that it's submitting like an image as part of the form POST, a multipart form POST or something.<br>
It's, it's reading the stream, so we want to make sure we don't block during that.<br>
So in order to do that, we're going to get to one of the very exciting features, our first chance to touch it, and we're going to dive deep into this on the database side, it's we're gonna use async and await.<br>
So to convert this task, this potential result to an actual thing, but allow other work to happen at the same time.<br>
We have to say, await that.<br>
And now if we go to form, you can see we have a multi dict form data type of object with keys and items and dictionary stuff.<br>
But in order to use await, we have to have an asynchronous function up here, and in order to do anything useful with our asynchronous function.<br>
Maybe if I left it like this, what you would see, you would see that there's a there would be something like, this was not awaited, or something like that at the end of your code.<br>
If you see an error like that, that means you forgot an "await" somewhere, and we need it there.<br>
And again, in order to call this here, we need to make this in an async function.<br>
And here's where it gets awesome, FastAPI automatically supports both synchronous and asynchronous view methods.<br>
So this is no problem.<br>
We don't have to do anything else.<br>
We don't have to choose a different server or a different way to run FastAPI.<br>
There's one reason, Uvicorn, as the primary server for it, all of this stuff just automatically adapts.<br>
But once we're down here and we start down this path, we've got to cascade the asyncs back up.<br>
OK, so we've got that done.<br>
Let's go ahead and get these things, the password and the email.<br>
The other thing we need to do here, though, is we need to check, we need to check that these values are set properly, so we'll say, if not self.name.<br>
Now we could put validation, and we will put validation in the form that says the name is required, but there's nothing that forces the browser to follow it.<br>
If you're using it normally, of course, Chrome or Firefox will say this form field is required.<br>
But if, what if you just grab something like requests and do a POST against this to see what's gonna happen?<br>
Well, none of that validation is gonna happen, so we have to make sure we re do that here.<br>
So if name, there's no name or not self.name.strip() like a space or tab won't do it.<br>
I'm gonna say self.error equals the name, your name.<br>
And we're going to do something like very, very similar to this for the others.<br>
That did not work out as smooth as I hope it did.<br>
There we go.<br>
elif email, your email is required.<br>
And then we could do password.<br>
Let's change this a little bit, say, or the length of the password is too short.<br>
Fewer than five characters say: "Your password is required and must be at least 5 characters" I think we might be ready.<br>
This is the processing and validation side of our form, and we could do could, we could grab that age and say we want to convert that to a integer, and if it's not an integer, it has to be an integer.<br>
We'll see some other stuff that we could do around that as well.<br>
But I'm just gonna leave it like this for now.<br>
So now the next question is, did that work?<br>
We'll say, if, how do we know?<br>
Well, one way is that there is now an error contained in the view model We could also have this return, you know, return False right if we want.<br>
But I'll just check whether or not there's an error.<br>
So if there is an error, we want to do exactly this right here.<br>
Otherwise I'll just do a print really quick.<br>
"TODO: Redirect" because this is, it takes a little bit of juggling here.<br>
So we're gonna do that in the next video, and I'll just print this out, like so.<br>
Just return it back again one more time and we'll change this last line.<br>
So what we're gonna do is, it's gonna come in, submit the data.<br>
We're gonna load it up, grab the form asynchronously read it, validate it, store the data.<br>
If things are not good, instead of just returning, we want to return the data they submitted back to them to reload that form with what they've typed.<br>
So if you say your email is wrong, we'll leave the one they typed wrongly there so they can make adjustments to it rather than make them fill out the form from scratch.<br>
Give this a shot, see what we get.<br>
So if we come over here and we click on register and if I just submit nothing, I'm gonna put something there really quick.<br>
Submit nothing.<br>
Oh, something has gone wrong here.<br>
What has happened?<br>
Yes, I meant to mention this earlier.<br>
When you work with forms, just like when you work with templates or you work with static files, there's other dependencies you need.<br>
And when we work with forms, this Python-multipart package is required.<br>
Well, let's go install that.<br>
Then I think our form will actually be ready to go.<br>
Install this, there we go.<br>
Give it one more try, resubmit it.<br>
Look at that, "Your name is required".<br>
How awesome is that?<br>
So now if I put my name and I try to submit it, "Your email is required".<br>
OK, put my email, password, now great, let's put the letter "a".<br>
Nope, must be 5, 5 "a".<br>
1, 2, 3, 4, 5.<br>
All right, here we go.<br>
It worked, the only one way we know that worked is one, we saw the "TODO: Redirect" here.<br>
The other is the error message went away.<br>
Super cool, right?<br>
So if we didn't resubmit that dictionary right here, every time the form would empty itself.<br>
But we're reloading the form every time with whatever they passed in.<br>
So in case this was missing, it's going to say, it keeps the Michael and the password and it just says, just put your pass, your email back.<br>
Really, really sweet way to do this.<br>
GET > POST, almost redirect pattern.<br>
Virtual redirect.<br>
Here, looks like we got it working.
|
|
show
|
3:39 |
Well, we've got our form taking the data and validating it, but it just stays right here.<br>
So there's a couple of things we still need to do to make this work.<br>
One is we need to go over here and say TODO:, Create the account, and let's go and write some code to do it and then we'll make this actually happen when we get to the database.<br>
This is gonna be the user_service, and we'll say create_account() and we need to pass all that information over.<br>
So view model is going to have the name, the email and the password.<br>
That's going to create an account and we probably also want to say TODO: Login user, which we still got to get to in a moment, do that.<br>
And then we want to do the redirect, right?<br>
We're still kind of focused on this GET > POST > Redirect and this the last step in that.<br>
Let's go ahead and write that function real quick so it doesn't crash, it's just gonna be strings and it's gonna return one of those users, one of these classes, and we'll just have it return the user like this for a minute name, email, "abc".<br>
We've gotta hash the password, not for the moment, but we'll get back to that.<br>
OK, So we're gonna log in our user and then we want to redirect.<br>
Now, remember we talked briefly about FastAPI responses, responses there.<br>
There's a bunch of great stuff, we started out by saying, oh, there's this regular response and the JSON response, and let's use an HTML response for a minute.<br>
You can see these actually come out of Starlette, not FastAPI directly.<br>
So we've got a perfect candidate for our redirect.<br>
We're gonna return a RedirectResponse, and what could we put in there?<br>
the url, the url is going to be equal to /account.<br>
Let's give this a shot and see what happens.<br>
All right, come over here.<br>
We're going to submit this form, it's gonna fake out creating that account, and then it's gonna redirect to /account.<br>
But it's not gonna work the way you're expecting.<br>
You probably think we'll just see the account page shown here.<br>
But let's see what happens.<br>
Method not allowed.<br>
Wait a minute, what just happened?<br>
what just happened?<br>
Look at this, it did an HTTP POST when we did the redirect from /register it redirected as a POST.<br>
Most frameworks automatically convert redirects to GET, FastAPI and Starlette do not.<br>
So no problem, what we can do is, we can go over here and we can actually import status.<br>
We can set the status_code equal to status, and this is going to come out of Starlette.<br>
And here we have all the HTTP statuses and the one we want is 302 Found, and that means redirect with a GET.<br>
Right, one more time.<br>
Go back here, try to submit my form.<br>
Should see it, redirect, and off it goes.<br>
Tada!<br>
there we go.<br>
And we've got the correct redirect instead of one of those 307 or whatever it was there.<br>
302 Found when I did the POST to register and then it did the GET to the account.<br>
So GET > POST > Redirect.<br>
Perfect, so now we've got this working and you can imagine something very, very similar for login, right?<br>
We're gonna create a GET version, a POST version.<br>
The view model is gonna load the data out of the form, validate it, do some stuff like log in the user, make sure their account exists, log them in and then do a redirect, probably over to the same place as well.<br>
So this pattern will serve us for most of the HTML forms that we wanna work with.
|
|
show
|
3:56 |
Let's talk a little tiny bit more about validation.<br>
We saw that we're getting lots of validation in this section here on the server side, but a better experience for the user would just to be have the form as they type, give them a little feedback.<br>
Like hey, you know what?<br>
Your password has to be five characters before you hit submit, your email field has to be an email, not just some random text, those kinds of things.<br>
So in order to let you see the server side validation, I turned off the client side validation.<br>
You might have noticed it was here actually before, but what we're gonna do is we're gonna do a couple of things just in HTML to make this a little bit better.<br>
So let's just check, for example, if we go register.<br>
I try to register, it's posting back, and if I go Michael and it's really quick, so it's not a horrible experience, but if I go and type this, oh it's required.<br>
How about that?<br>
No.<br>
Still no.<br>
And it would be nice if it would give us information like if I just type this, it would say: oh, you know, that's not a real email, you gotta do better than that.<br>
So let's make just a couple of little changes.<br>
I already pointed out that you can't rely on this to validate all your data.<br>
Somebody could entirely skip this and just use Requests or something like that to jam data straight at your server.<br>
So you gotta have service side validation, always.<br>
But you can make the experience nicer.<br>
Like I can come over here and say, in this section here, that this is required, right?<br>
So all these fields that are required, we just say they're required and all we've gotta do is put an empty attribute They don't have a "values" or anything like that.<br>
And then, we try again.<br>
We refresh, without a POST.<br>
We just try to submit it, notice all the things are in red, it says this one is required, put stuff in there and you see the red goes away as I work with it.<br>
Really nice experience, right?<br>
OK, so that's great.<br>
Let's put in the letter "a", gotta put a number into this one now, password's required.<br>
Now we went back to the server, but we can do more.<br>
Let's have another look, what else could we do?<br>
Well, did it let me type in junk for the email?<br>
It did.<br>
That's because we said is this is the "text" type, but it also, there's many other types, like email, months, numbers and so on.<br>
So if we say that this is an email, and this age.<br>
You know, the age is already number so that's good.<br>
So it'll make sure it is a number.<br>
We'll be in better shape here.<br>
Actually, that's not.<br>
Submit it again.<br>
So if I say my name is Michael and I just put Michael here and the password is "a", notice, it's trying to, trying to suggest something.<br>
What is the problem here?<br>
Yeah, I don't know if I can get it to show you but it says this must be an email.<br>
So if I put some, oh there we go!<br>
Perfect, finally got it to come out.<br>
So if we go and put that in there, it'll now the red goes away.<br>
This must be a number, so I'll say that.<br>
And this one, it tried to submit it back.<br>
We can do more.<br>
Still with this one, we could say at the client side, this must be five characters.<br>
Over in the password we can say minlength has to be five.<br>
That's good.<br>
And then also, while we're at it, let's say that you have to be 18, at least to register.<br>
So what we can go over here and say that the minimum for this is 18 and let's go and set a max so it's a reasonable age, 120 or something like that.<br>
All right, let's go look at this again.<br>
All right, gotta fill out our name.<br>
Sure, we can do that.<br>
With the letter "m", "a".<br>
Nope, it's gotta be an email address, email address.<br>
Okay, it is.<br>
The password, my favorite, the letter "a".<br>
You're currently using one character.<br>
OK, That's right.<br>
It's five a's and then down here, it says your age has to be a number.<br>
but watch if I hit up arrow.<br>
Remember?<br>
It did 1, now does 18.<br>
If I hold it down, he goes up to 120 and if I say 121, no, no, it's too big.<br>
Let's go 40 or something reasonable.<br>
Now we can register.<br>
We got a chance.<br>
That's the first pass of all this validation right on the client side, users get a little better feedback as they're working on it.<br>
And then off it goes.<br>
We've registered, redirected over to the account, tada!<br>
we've got our form working really, really well.
|
|
show
|
5:50 |
Got a little bit more work to do here, and I guess we could take away the TODO on this one.<br>
But now we're gonna need to log in the user.<br>
How are we gonna do that?<br>
Let's go and create a special class that manages putting the user information into cookies and getting it back out of cookies, checking what is there and so on.<br>
Because we'll see there's a naive way we could do pretty quickly inline here and a more complex way, that works a lot better.<br>
So let's go create a new category of code called infrastructure.<br>
This is like stuff that helps out the rest of the site.<br>
Little utilities like converting integers or pulling out cookies or stuff like that.<br>
And I'm gonna call this thing, we're creating cookie_auth.<br>
What we're gonna do is we're gonna take two passes at this.<br>
I'm gonna show you a simple version that has a problem and then a more interesting version that we're just gonna drop in here and review that's like this, but better.<br>
So we're gonna have to functions here.<br>
One I'm gonna call set_auth and it's gonna take a user_id and all it's gonna do, it also needs a response, which is a Response from Starlette, and this is gonna be an integer.<br>
Then we have another one called get user id from cookie, from auth cookie.<br>
And I think this is gonna need a request.<br>
Same one that we've been working with the whole time, from Starlette.<br>
And we don't pass in the user_id because we don't know, we don't have it yet.<br>
That's what we're doing here.<br>
It's gonna be Optional integer from typing, perfect.<br>
So let's go over here and use this cookie_auth, so cookie_auth and I need to pass it this before we send it along.<br>
So there's a response.<br>
Here's our redirect.<br>
And then we need to say set this cookie here before we send it along.<br>
So the response is gonna go there, and the user_id, we'll say account.id, doesn't have an id yet, but we will go and update our class to have one.<br>
So do that real quick.<br>
Let's say it's always 1, until we get to the database where it'll autoincrement.<br>
Alright, so I'm gonna pass that along and then it's gonna be really simple.<br>
All we gotta do is go to the response and say, and our first pass is gonna be simple.<br>
At least set_cookie, the key, let's make sure we always use the same key.<br>
So, like the name of the value in the cookie dictionary, this will be auth_key.<br>
It's gonna be, let's say, 'pypi_account' like that.<br>
So the name is gonna be that, the value that we wanna pass in is the string of the user_id, the max_age.<br>
Ah, we don't need to set the max_age, there are a couple of settings we do wanna set though, we want to set secure, and we're not going to set it here, I'm gonna say False.<br>
But in your real web app, when you're running in production, you wanna set this to be True so that if somebody types your domain without using HTTPS, it won't still exchange the cookies over HTTP, where people could eventually watch it.<br>
But I'm gonna say False in development because we don't have SSL set up in development.<br>
And then httponly, this means that JavaScript code cannot read the cookie.<br>
It'll exchange it, but it can't read it.<br>
So I'm gonna say True and samesite being "lax", that's good.<br>
All right, I think we're in good shape with this.<br>
And then over here, we wanna read it back.<br>
We wanna take our request.<br>
I'll say, if auth_cookie not in request.cookies, then we'll return None, there's no value here.<br>
And let's, for now, just say we're gonna directly store this as a string, so we'll say user_id equals, notice it's dictionary-like so the way we access it is auth_key like this.<br>
Then we need to convert this to an integer like so and return it.<br>
Right, so what we're gonna do up here, we're gonna call this, which we are.<br>
And then let's go and actually make our view model Do the magic that it was supposed to do all along.<br>
Remember we said you're logged in, yes or no.<br>
Let's go like this, Let's say cookie_auth, get user_id from cookie self.request so the request is passed in.<br>
All right, and if that comes back with something, well, we should be logged in.<br>
If it doesn't come back with anything, we won't be logged in.<br>
Let's go ahead and give this a test and see how well we've done.<br>
But first of all, notice we're not logged in, so this "Login" and "Register" are set, that's a good idea, a good sign.<br>
Let's go over here and register with all the info that I put in.<br>
Oh, yeah.<br>
I need my five a's and let's just put anything there.<br>
So this works, it should set a cookie.<br>
Let's go over here and see if we can get it to show us while we're at it.<br>
It should set a cookie, then redirect us to the account page.<br>
The account page, all the pages, will look and see if there is a login cookie and should switch this to "Account" and "Logout".<br>
If it doesn't do that, we got something wrong.<br>
Let's see how this works.<br>
Off it goes, yes, perfect.<br>
So check this out, "Account" and "Logout" are now set over here.<br>
And here we go.<br>
We've got our account information there, if we go and look at this you can see set-cookie pypi_account, is id equals 1, HttpOnly, Path is what it is, SameSite is "lax", like we described.<br>
Then when we did a GET over here, the response had over here in the cookies, had that pypi_account is one.<br>
I think we've got this working.<br>
We've got our cookie set up.<br>
At least in this naive way, we just have that stored in a cookie.<br>
And if it's set, then we're logged in.<br>
We pull back that user_id, if it's not set, hey, we must not be logged in.
|
|
show
|
3:26 |
Let's have a look at our cookie_auth one more time.<br>
So this looked great, right?<br>
We saw that we're storing our account id in the cookie.<br>
And if we submit that cookie, then everything's golden.<br>
What happens if we were to try to play with that?<br>
What if we were to go find the cookie on our disk?<br>
Or we were to do some sort of POST where we set a cookie?<br>
We noticed well, if it was 1, what if it were 2?<br>
what if it were 100?<br>
Could we be other users?<br>
How about that?<br>
That'd be cool.<br>
In general, more broadly, the problem is, what if people tamper with the cookie?<br>
What if they make changes to the cookie?<br>
How do we know?<br>
we wouldn't know.<br>
I mean, as long as it was valid data, it's a valid user, it'll look like that user logged in.<br>
So what I'm gonna do is drop something in there that takes the data and then creates a hash of the data.<br>
When we get it back, we'll be able to say, here's what they gave us.<br>
Here's what it should look like if we were to scramble it up, do they match?<br>
Right, so we're going to store both, the value and the scrambled version, which they won't know how to recreate because of the way we've generated it.<br>
And that'll give us a tamper proof digest type of thing.<br>
So I'm going to just drop that in here, and now we've got a slightly more complicated version.<br>
So let me just walk you through.<br>
It's basically the same thing.<br>
So when we get a user_id passed over, instead of just sticking the user_id directly, we're going to hash this user_id in a certain way that's hard to recreate and put the user_id here.<br>
So if somebody gets hold of the cookie, they see the number one and some giant scrambled thing.<br>
That way they'll not be able to look at it and say, Oh, I can just tweak this because if they tweak it, they'll have to adjust the hash, the match to be the same.<br>
So we're gonna create a sort of a combo here, and then when we get the cookie, we're gonna pull it apart and we're gonna check two things, give us the value and the hash value, and do those match as we would expect?<br>
They don't, we get nope, there's no user here.<br>
Otherwise we're gonna convert it to an integer.<br>
OK, To hash it, what we're gonna do is a simple sha512, but we're also going to apply some salt at the beginning and the end.<br>
So instead of just hashing the number two, we're gonna hash "salty__", two underscore, "__text".<br>
Looking at the value, you would never know that that's what you need to hash.<br>
So it provides a small level of safety, not huge, but it does help us here.<br>
OK, So what we're gonna do is we're gonna try this again.<br>
I'll that "pypi_account".<br>
All right.<br>
So make sure I named everything correctly.<br>
No, I didn't.<br>
Let's see, get_user_via_auth_cookie, there we go.<br>
That's what I dropped in there had the name of and then over in account.<br>
Looks like this is working, so let's run it.<br>
We should not be logged in when we first get there.<br>
We're not, go register.<br>
Takes five a's and an up arrow and we should be able to register.<br>
Off it goes, perfect.<br>
Now we're logged in, and let's look at the thing we exchanged this time around Network, HTTP, go anywhere and we should be able to see our cookies getting sent back.<br>
Here we go, now check that out.<br>
We got the "1:" and then that huge blob of stuff, Right?<br>
That is the verification that if we were to try to change that, which guess what?<br>
Go ahead and make changes to it.<br>
If we want, we're not gonna be able to change it, right?<br>
If we put a 2 there, we would have to know what that huge thing on the end that corresponds to the new hash.<br>
So it'll make it much more tamper proof.<br>
And that way people won't be able to go randomly, change stuff around and cause problems with our site.
|
|
show
|
1:23 |
Let's take this whole experience full circle and add the ability to log out.<br>
So we're able to register, then we're logged in.<br>
How do we thought being logged in?<br>
How do we do this logout?<br>
Well, it turns out it's incredibly simple.<br>
So when we logged in, what we did is we created this response and then we set some cookie.<br>
So here, when when you do a GET, it's just going to do the opposite of that.<br>
So we're gonna come along and grab this.<br>
It's quite similar.<br>
And instead of sending them to their account, which doesn't make sense because they're not logged in, let's just send them back to the home page.<br>
And we're gonna again use the 302 so we don't do a POST, we do a GET.<br>
And over here, this actually has a logout where you just pass in the response.<br>
If you look at that, it's incredibly simple.<br>
It just goes to response, delete cookie, the name of whatever we called the cookie.<br>
Let's go ahead and try to rerun this, see if we can now log out.<br>
We click here, we're still logged in, we still have the cookie, we go to uvicorn, we're still logged in got to account.<br>
But now, if I hit log out, it should delete that cookie and send us home.<br>
Let's see if we got it right.<br>
Boom!<br>
Cookies deleted, logged in info is gone.<br>
And now we're back home where we're meant to be.<br>
Right?<br>
That's it.<br>
So we've got our GET > POST > Redirect pattern.<br>
Think in the form, submitting it, once we're registered, and validated, we create the account, we set the cookie, hangs around for a while until we log out and then it's gone.
|
|
show
|
3:11 |
Because login and register are so super similar but require a lot of juggling of little pieces, I'm just going to drop this little bit of code in here and let's review it and add the final pieces.<br>
So just like register, we're going to have a GET version and we're gonna have a POST version and then do a redirect.<br>
In the GET version, We're gonna have our LoginViewModel, and then it's just gonna return the empty form.<br>
Let's look at that really quick.<br>
It's gonna have an email and a password, as you can imagine, along with the base error message that might be sent over, like you couldn't log in or wrong password or something.<br>
And we're gonna do a load in our POST back to get that from the form, just like we did before.<br>
Over here, we create it, and this time we await a load, which means it has to be async up here.<br>
This one doesn't actually have to be async, I guess, because we're not doing async stuff.<br>
And we check for errors.<br>
If there are, we reload the form with the same data, show the error.<br>
We're going to need to write a login_user, given the email and plain text password they submitted.<br>
If it didn't work, then sorry we couldn't get that account.<br>
Either it didn't exist or the password doesn't work, match or something.<br>
You want to do a redirect with 302, set the auth cookie just like we did before, and send that response back.<br>
One thing we could do real quick here is we can create this function, it's going to return an Optional of User like so, like that.<br>
Now, the test we're gonna do this first time around is really, really simple.<br>
So remember, we passed in an email and the password, which is 'abc', that we stored?<br>
So let's just check against that.<br>
When we get to the database section we're gonna actually store the hashed password in a really cool way.<br>
But let's just go and add a simple test.<br>
We'll say if password equal equal, let's say "abc", then we'll return some basic user like this.<br>
The email's what they passed in and the name will be "test_user" or maybe that, more friendly name like that.<br>
Otherwise, what we're gonna do is return None, because that didn't match, right?<br>
Either we didn't find it out of the database or they typed it in wrong.<br>
Let's go just run through this experience real quick to make sure our login works in a simple way.<br>
So here we are, we're not logged in currently.<br>
We're gonna log in, the password, the password is gonna be "ab", which is not "abc".<br>
So this should not let us in.<br>
"The account does not exist or the password is wrong." Oh, let me try that password again.<br>
That's right, it was "abc".<br>
Now it should set that cookie log us in, redirect us to our account page, and the navigation up here should say "Account" and "Logout".<br>
Boom!<br>
just like that, it does.<br>
Really, really similar to the registration stuff, but we try to find the account instead of trying to create the account.<br>
Go log out and we're back logged out.<br>
So we have all of our account management stuff besides actually saving the users in the database, which is the next chapter.<br>
But we've got all the general HTML view, view, model flow and validation of account management and HTML forms completely dialed
|
|
|
1:01:13 |
|
show
|
2:14 |
It is high time that we start working with a proper database.<br>
There's been way too many times that I've had to say: "And we'll actually create users and save them when we have a database, we'll actually pull real package information, when we have a database." So that's what this chapter is about.<br>
Were gonna put that database and that data access layer together with SQLAlchemy.<br>
Now, notice it's not just databases with SQLAlchemy, this is "Databases with SQLAlchemy Sync Edition" as in synchronous, not asynchronous.<br>
We're gonna talk about SQLAlchemy in two passes.<br>
First, what I believe to be the familiar and common way of working with SQLAlchemy.<br>
And then we're gonna come back and work with a new A P I that they're working with in a beta mode, it's not even fully released yet.<br>
Why are we doing this?<br>
We're doing this because the traditional way of working with SQLAlchemy in no way supports asynchronous programming.<br>
And one of the massive, massive benefits of FastAPI is it's natural and easy use of async and await.<br>
If you want your application to scale massively, you need to make sure your data access layer allows you to write asynchronous code.<br>
So every single time that you're waiting on the database, your web server's processing more requests instead of just blocking.<br>
So in order to make that happen, we need to use the asynchronous version.<br>
But that's in beta mode, and I don't think a lot of people have really any experience with this new and not compatible way of writing.<br>
This is why I pushed the database section further back in the course, because we have this sort of complication of like we're working with two APIs, one of them is under beta.<br>
It's not a big deal, but I didn't wanna throw all that complexity, as it doesn't really have that much to do with FastAPI directly, at you before you've seen most of what we're doing with the web framework.<br>
So that said, we're gonna in this chapter build out the traditional synchronous version and then we're gonna convert that to the new asynchronous API.<br>
The reason I'm doing it that way is cause I think most people are familiar with this style of SQLAlchemy and the new style of SQLAlchemy will be uncommon, unfamiliar with people.<br>
So I want to give you a first past where you're like "Oh yeah, I see what's happening, I know what's going on" and then we can come to terms with the slightly different async style of working.<br>
Let's get to it.
|
|
show
|
2:05 |
When I introduced this chapter, I said, you're familiar with this SQLAlchemy API and we're going to start that way.<br>
Well, what if you aren't familiar with it?<br>
What if you need to learn SQLAlchemy?<br>
Maybe you're brand new to databases.<br>
Well, there's a couple of things you can do.<br>
One, you can just sit back and let it wash over you and kind of get an appreciation and some initial exposure to what we're doing.<br>
Or if you want to make sure you dive in, you wanna do a little extra work, I'm going to actually put an appendix that you can go study first, before we jump in to actually writing the code.<br>
If you look over on the Talk Python Training site, we have a class called "Building Data-Driven Web Apps with Flask and SQLAlchemy".<br>
In this course, we built PyPI from scratch but way, way deeper into the web design, the web development foundations across everything.<br>
Remember, this is about teaching you how to do that kind of stuff with FastAPI, not how to teach you how to do it from absolute no understanding, which is kind of what this course does.<br>
And if you look down here, you can see we've got chapter 9 and 10, "Modeling data with SQLAlchemy classes" and then doing basically queries, inserts and updates with SQLAlchemy as chapter 10.<br>
It's almost two hours of content, I'm going to take those two chapters and put them into appendices at the end of the course.<br>
If you're really wanting to go slow and take everything in and SQLAlchemy this is your first exposure to it.<br>
Then go ahead and watch those two appendices, those two chapters.<br>
Not only will they teach you SQLAlchemy in the context of web development, they will teach you SQLAlchemy building PyPI.<br>
So, in fact, the data model is identical.<br>
So in this course we built pypi.org from scratch, using much of the same constructs.<br>
And over here, when we talk about SQLAlchemy, we're talking about the same thing users, packages, releases and so on.<br>
So you'll get a really good foundation for what we're gonna do with SQLAlchemy over here if you'd like to spend an extra couple hours digging into the foundations.<br>
If you're familiar with it, you probably don't need to do this, but if your brand new, your time is probably well spent over here going through the two appendices before we go further.
|
|
show
|
4:06 |
Before we start writing code.<br>
Let's just take a really quick look at some of the differences of the current SQLAlchemy and where we're going and you'll see how we're gonna write code now and then in the next chapter, we're gonna write it against the new API in just a little bit of detail.<br>
Here we are at SQLAlchemy.<br>
There's currently a 1.4.0b1 released about two months ago, and this is the culmination of about 18 months of work.<br>
And the main goal of this is to bring a new API, which will be introduced maybe exclusively in SQLAlchemy 2.0.<br>
So when we get to 2.0, we'll have to switch to this new API.<br>
Let's just really quickly look at the announcements here.<br>
So it says this is the first beta release of the SQLAlchemy 1.4 series.<br>
It's the culmination of 18 months of effort to envision and create a modernized, newly modernized and capable SQLAlchemy.<br>
The first step along the way to SQLAlchemy 2.0, and includes a gradual introduction to the simplified way of working with, most importantly, the ORM So if you just look at some of the changes, there's a simple and more consistent way to work with transactions.<br>
There's this select() construct that gets used now heavily in the ORM where it used to be you would just go create a query and I believe the select stuff was more in the "core", if you wanted to do direct queries some caching, which sounds amazing.<br>
Here's why we're having this conversation, why this is valuable.<br>
This is the first version of SQLAlchemy to provide support for a asyncio, meaning async and await and direct integration with FastAPI.<br>
So that's gonna be super, super awesome, that we can use the ORM with async and await to get massively scalable applications all the way down to the database.<br>
OK, so that's what the goal here is.<br>
We're gonna go through and create everything in a familiar way, and then we're gonna make just some changes, really only to the queries that we write, it won't be that big of a deal, but you know there it is.<br>
Let me just show you what some of those differences look like.<br>
So if you go back here to the home page, real quick, just to give you guys some guidance here, there's this releases.<br>
Oh, look, there's migration notes and there's a big, long document about it.<br>
But if you look, there's another migrating to SQLAlchemy, which is not those migration notes.<br>
It's a totally different one.<br>
You can see the URL appear at the top.<br>
Anyway if you, it's really if you look at the scroll bar, it's quite a large document.<br>
If you go down, now what is that?<br>
About 70% of the way down.<br>
It talks about the biggest change in SQLAlchemy 2.0 will be the use of Session.execute().<br>
Instead of using query, you're going to create select statements and then execute them.<br>
What's nice is down here we have a couple of examples that we could use.<br>
So traditionally, you would write something like, I want to get all the users, session.query(User).all(), now you write this, you execute a select statement on the user where you get the scalars and then you call all().<br>
Down here you wanna do a filter.<br>
So give me the users, so session.query(User) where the name equals "some user", give me the first one.<br>
Now you'd write, you would execute the statement, select user, filter by name, scalar_one(), ok?<br>
You notice that this one applies to the query, whereas here, your select and your query is one thing and then you execute it against the result of the execution.<br>
So there's a little bit of difference going on here.<br>
So you can just scroll through this and see that these APIs are not even close to similar.<br>
Maybe there's some parts that are similar, but to me, they feel fairly different.<br>
Like all() is now scalars().all().<br>
When you do a join, you have to do a unique() on it.<br>
So there's just a lot of stuff that is really not directly transferrable.<br>
You know, it's not necessarily obvious that that's the adjusted thing.<br>
So what we're gonna do is we're gonna create this, this version that I believe most people who work with the ORM of SQLAlchemy totally have down pat.<br>
And then we're going to rewrite the queries in this, not the classes, just the queries, and make those asynchronous which is gonna be so worth it.<br>
And we're gonna do that in these two chapters.<br>
Here's the documents that you might wanna look at.<br>
So we've got, you know, https://docs.sqlalchemy.org/en/14/changelog/migration_20.html.<br>
All right, and there you can find this comparison doc.<br>
It's about time to write some code, don't you think?
|
|
show
|
9:19 |
Welcome to chapter seven databases.<br>
Copy over the final result from Chapter 6 to 7, set it up as a new project and open it here so you'll have what we finished with in 6 as well as what we're doing.<br>
Here we go.<br>
Let's open it up and get going.<br>
First thing we need to do when we work with SQLAlchemy is create this thing that will create what are called sessions or units of work.<br>
In order to do that, we've got to create an engine which binds to a database connection string.<br>
There's all that kind of initial set up.<br>
We want to create the table structure based on the classes, and that's done through common couple of lines of code that always gets done once, and exactly once, only once at startup for the application.<br>
You've probably seen SQLAlchemy before or you've watched the appendix series that we talked about that, that does this.<br>
So let's go over here.<br>
I'm gonna create a class.<br>
Really, I'm gonna create a module, excuse me, db and call it db_session and in here I already have some code.<br>
I want to just walk you through because it's really not super valuable for us to take the time to go through all the details here.<br>
And if you're familiar with SQLAlchemy, you'll know it.<br>
We don't have SQLAlchemy set up as a dependency yet.<br>
PyCharm will actually do that for us.<br>
It says, you want to install that?<br>
Yes, please do.<br>
And then, once that's installed, it will say, you know, you really should add that to the requirements.<br>
And I'm gonna say give me a compatible version.<br>
What I've done so far with, I'm going to say, give me the exact version, actually.<br>
What I've done so far is just left the dependencies and versions open.<br>
That's usually the best, you'll get the latest.<br>
But with this 1.0 - 2.0 difference, I'm gonna be really careful.<br>
So what we're gonna do is, I'm gonna pin it to this exact version.<br>
You can put it on, whatever the latest version that works when you get a hold of it.<br>
But for now, I want to make sure if you check it out and run it, it's gonna work now.<br>
And what we're gonna need to do is put 1.4.* something, 1.4.* something, or higher when we get to the next section to enable the async and await features as well.<br>
So, here we are.<br>
We've got that specified and saved, so it's easy to get this back.<br>
All right, so there's one other thing that we're gonna need in order to work with the ORM, and it's we're gonna need to define a class called whatever you want.<br>
But I'm gonna call mine SqlAlchemyBase and we're gonna put it into a module called modelbase.<br>
Let's do that first and we'll talk through this file here.<br>
So I'll call it this, and we'll say import sqlalchemy.ext.declarative and we'll come down here and I will say this is gonna be just call declarative_base() and that's it.<br>
That's all we gotta do.<br>
We just need this dynamically created base class to exist.<br>
And actually, you can create multiple ones and associate different connection strings and databases with every class that derives from this.<br>
So now we've got that in place.<br>
Let's talk through this.<br>
So what we need to do is at the beginning, we're gonna use SQLite and SQLite takes basically just a file as a connection string.<br>
If you had PostgreSQL or Microsoft SQL Server or whatever else you want to use, they have their own way of writing a connection string.<br>
For us, we're gonna use SQLite, so we're gonna specify just a file.<br>
We're gonna check that If we've already done the set up, not gonna run a second time, it'll make sure that there is a file, it doesn't have to exist.<br>
SQlAlchemy and SQLite will actually create the file if it doesn't exist.<br>
But one thing that we do want to make sure is the folder structure where you specify it does.<br>
So down here you can see mkdir, I'm using this pathlib thing say create the parents and it's fine if it exists.<br>
Going to say "sqlite:///", file name.<br>
That's the SQLAlchemy connection String for SQLite.<br>
Then just shoot out a quick note about what's happening here.<br>
Then, we're gonna, we're gonna go and create an engine with that connection string.<br>
If you want to see every command that goes to the database, set that to True, it's super chatty, but it does give you a good sense of what's happening and then SQLite has, sometimes complaints about threading, but it's also thread safe, so we can just tell it not to complain to us.<br>
And then what we're gonna do is create this thing called a factory and that job is to create these units of work or this sessions, as you can see down here, it's we just call this function, creates one of these by calling it, this is a handy little thing that makes working with fields after you've saved them easier and then we're gonna return it.<br>
OK, The last thing to do is we need to make sure that SQLAlchemy, the base class has seen everything that derives from it.<br>
And the way that we're gonna do that is, we're gonna go over here and create a file like this, and there we'll say from data.package import Package and data.user import User and release as well.<br>
These are not SQLAlchemy classes yet, but you know what?<br>
They're gonna be really soon.<br>
So let's get this in place.<br>
Now, PyCharm is usually super, super helpful here, it says, look, you're not using that.<br>
If you wanna go over here, you could Alt, Alt + Enter and it would go away because you're not using it.<br>
But in fact, we do wanna have this exist for this particular file only because the whole point is just to do an import.<br>
And that's enough to trigger what we need for this base class to see that it derives from those various other classes, as it will shortly.<br>
And then finally, we call create_all on the engine, and it's gonna go create the database for all the tables that don't yet exist but have classes.<br>
It will create those, it won't make updates to them, so be aware of that.<br>
But it'll create all the tables in this db_file that we're going to have here.<br>
All we gotta do now is make sure that we call this and it should get our database all set up and ready to go.<br>
Remember, this gets called once and only once.<br>
Hence the global_init not all_the_time_init.<br>
Let's go down here and add another thing to our config.<br>
See these start to grow, those will be configure_db and what we need to do here is we need to come up with a file name or for the SQLAlchemy db_file.<br>
So what I want it to look like is I want it to be in the working directory, then db then pypi.sqlite like that, and an easy way to get a hold of that is we can go to the main file, Path, we'll import that from pathlib and we say it's the current file, so that's the full path to main.<br>
And then from here we can say, parent, that will give us this directory and then we can say "/".<br>
It's kind of funny, they override the divide operator to look like path concatenation, but it works.<br>
Then we can say pypi.sqlite like so.<br>
And then all we've got to say is file equals that, and that should be what we get, what we want here.<br>
And we also want to have the absolute path.<br>
I guess we could, do I need to do it like this?<br>
I'm not sure, whether I'd rather do it in a second line or do it like this, but that'll give us the absolute path.<br>
And let's just print file for a second.<br>
OK, make sure we call over here configure_db(dev_mode).<br>
We don't really need to use the dev_mode here, but let's go and make it available.<br>
We could do things like turn on the SQL tracing or turn it off.<br>
Let's go and run this and just see we've got the file working.<br>
And here's the path that it found.<br>
So here you see we have courses/fastapi/fastapi-apps/code/ch7...<br>
and I got the full user name there, chapter seven, then db,/pypi.sqlite, perfect.<br>
That is what we wanted.<br>
So instead of printing out, let's go ahead and call it.<br>
We'll say db_session and let PyCharm import that and then just say global_init(file).<br>
And over here, in a moment, we should be able to see a db folder created.<br>
If I got everything right, let's find out.<br>
Look at that.<br>
I always forget this when I'm working with this Posix pathlib objects.<br>
When we called this, we got a path object, when we called this, we got a path object.<br>
When we did this division combination thing, we got a path of object and so on.<br>
All the way to this file, it's still not a string, it's a pathlib object.<br>
So to actually get it as a string, there's two things we could do.<br>
We could say str() of it like that or we could come over and say as_posix() and that will print out, that'll give us just the string representation.<br>
The problem was global_init tried to call strip.<br>
If there is white space, maybe we read it from a config file, there's like white space at the end, or some weird thing like that, wants to guard against that.<br>
But it can't do that to a PosixPath, It can only do it to a string.<br>
So let's try it one more time.<br>
Oh it worked.<br>
OK, that's good.<br>
And look what we printed out: connecting to the database with.<br>
Here's our scheme that we need to use for SQLAlchemy.<br>
I noticed it looks like there's one too many slashes there, but there's not.<br>
The thing that we're passing here is just the full absolute path.<br>
So triple slash and then the path just happens to be that also has a forward slash.<br>
So it looks like we've got this set up correctly.<br>
If we go over here, we say reload from disk.<br>
We now have a db folder with a little SQLite database in it.<br>
It does not yet have any tables because the classes we've created are not actually SQLAlchemy classes yet.<br>
Once we create them, then that whole process will actually create additional tables.<br>
Right now, it's just an empty database, but still, that's pretty cool.
|
|
show
|
7:19 |
Now the next thing that we need to do is start creating some of these classes and we kind of went down this path a little bit already.<br>
Remember, I said, it's really tricky to pass the right data with the right, you know, properties and cascading properties and whatnot that we need to the package page and so on without just creating some classes.<br>
You know, all the time knowing that we're kind of headed towards the SQLAlchemy path, which is done with the ORM and classes anyway.<br>
So let's go and upgrade this class to be something that gets automatically queried and serialized out of the database rather than just an in memory class.<br>
So we're gonna use our SqlAlchemyBase.<br>
We'll just do it here like this, SqlAlchemyBase double Ctrl, double Ctrl + Space and we get all that magic happening.<br>
Now It's a SQLAlchemy table.<br>
Well, sort of.<br>
It doesn't have any columns yet.<br>
This is not quite the way you do that there.<br>
So let's do one thing before you forget.<br>
I want to set the __tablename__, something that bugs me about these things.<br>
Tables contain more than one thing, so calling it user to me seems weird.<br>
Should be "users", and capital, I don't dig that.<br>
So let's call this "users", lower case.<br>
I'll tell Pycharm.<br>
Now this is cool, you can have tablename all day long.<br>
It's not misspelled.<br>
Then let's add these fields, which are going to be columns.<br>
We're gonna have an id, which is a SQLAlchemy column.<br>
And one of the nice ways you can do this is, you can so, import sqlalchemy as sa.<br>
You can have a little bit shorter, something to write here, sa.column, whoops, capital Column.<br>
And then we have to specify the type, and this is gonna be an sa.Integer.<br>
Now, not just any integer, but this is gonna be the primary key, and it's gonna automatically update itself, increment itself on the database so we'll set two things primary_key is True, and autoincrement is True.<br>
So this should also create an index for us, we shouldn't have to have an index explicitly here cause it's a primary key.<br>
Next we've got name, which is an SQLAlchemy column of type sa.String.<br>
Let's just put the type information in here for now and we'll have an email, which is the same.<br>
We'll have a hashed_password, which is the same.<br>
We'll have a created_date and that is not a string that is a sa.DateTime.<br>
We'll have a profile_image_url, which is now back to the sa.String.<br>
Now all these done.<br>
And we've got our last_login, which is gonna be one of this, last_login.<br>
OK, super.<br>
Now, let's make this a little bit nicer to work with.<br>
A couple of things, how often do we want to be able to query a user by email?<br>
Well, when they log in with their email, very, very often.<br>
So let's go over here and say index=True.<br>
Also, if they log in and they say forget their password, we wanna let them press a button and reset their password.<br>
So we need their email to also be unique.<br>
So we come down here and say this must be unique=True.<br>
We can't have different accounts with the same email, that would just be weird, password, no index or anything like that.<br>
create_date, this one, when you create the user, we would like SQLAlchemy to automatically just set it to right now.<br>
So let's say the default, gotta be really careful here, is gonna be a datetime.datetime.now.<br>
That's what I wanna type, if I hit Enter, the parentheses go on there.<br>
And that is not what I wanna type.<br>
What I want to do is provide the function now, that SQLAlchemy will call.<br>
Not now() when the class gets parsed.<br>
So for the whole lifetime of the app, it's just when the app started.<br>
No, no, no.<br>
We do it like this.<br>
We can do the same here, the last_login, the very first time is right when they create their account.<br>
And then we're gonna have to set this the next time and the next time and the next time.<br>
profile_url_image, we don't really need anything on that either.<br>
Maybe we want to do queries like, show me the users who were created today.<br>
You know, show me all the new users order by descending on created_date or something.<br>
For that reason, we're gonna make this much faster if we have an index on those two things.<br>
I think that's it.<br>
I think that's it.<br>
Now, one thing you wanna be careful about is if I run this code, it's gonna create the users table.<br>
Once it does that, SQLAlchemy will never make a change to it again.<br>
It's like, that exists, we're not gonna break anything.<br>
So you want to make sure it's right, or you have to either throw away the database or directly edit it or do a migration or something.<br>
So, you know, double check the work.<br>
That look good, looks good to me.<br>
So let's go and run this and then we'll go inspect the database.<br>
Right, It didn't crash.<br>
That's a really, really good sign, because it did go through that global_init process.<br>
And importantly, it called this right, right here.<br>
SqlALchemyBase.metadata.create_all with the engine.<br>
And that'll create all the class, it'll take all the classes that derive from SqlAlchemyBase and create their corresponding tables and relationships and foreign keys and so on.<br>
We should be able to go look at our SQLite database over here.<br>
Let me reload from disk and see if anything changes.<br>
Yes, notice the icon changed now that it has an actual structure in it.<br>
It knows it's a database, that's super cool.<br>
What's not super cool is, I don't think that I can load it up here.<br>
Normally in PyCharm I could just drag and drop this over here.<br>
But notice there's issues.<br>
It's having issues.<br>
The reason it's having issues, I believe, is this is actually one of the Apple Silicon M1 Macs that I'm recording on, which is great, super fast, and this is the M1 version of PyCharm as well, the native Apple version.<br>
Everything sounds great, except for the SQLAlchemy database drivers don't work with it.<br>
So let me go and show you something else that's cool.<br>
If you wanna use that way, that's great.<br>
That's normally how I would do it in PyCharm.<br>
But if for some reason that doesn't work or you don't have PyCharm Pro, let me show you this.<br>
So we've got this really cool, nice looking database UI tool called Beekeeper Studio, and Beekeeper Studio is really quite a nice UI.<br>
Talks to all these different databases, lets you create different connection strings and basically do autocomplete queries and have a history of them, a history of the queries, explore the schema and so on.<br>
So what I'm gonna do is I'm gonna go open up this thing here.<br>
I can go SQLite, browse over to it.<br>
But you know what?<br>
I've already done this so I can just click right here.<br>
Click connect and check it out, we've got our users table.<br>
We've got an id, which is an integer, and our name and our hash_password, our created_date and so on.<br>
Now there's nothing in here, you can see if we go to a query, it's just empty, right?<br>
We could come over here and we could say "select * from users where..." You know, what have we got?<br>
a name, id, name.<br>
Really nice, right?<br>
We don't have any users yet, but you can see that it did find the structure of it and let us open that and explore it, which is pretty awesome.<br>
So this will be really handy for us to make sure we're getting the right databases created when we work with SQLAlchemy
|
|
show
|
5:29 |
So we've got our users created and we went real carefully through setting up the indexes and the primary keys and all that kind of stuff.<br>
Let's just quickly drop in an existing package and release and talk through it.<br>
There's really not a huge value in us spending tons of time working on the data model.<br>
So I'm just gonna drop in two files and we're gonna talk through them.<br>
All right.<br>
And this one is called "release" and this is called "releases".<br>
So let's delete that and then call this one "release" just so we get that right and consistent.<br>
OK, so releases first, it's a little simpler.<br>
Packages, there's one package, and there's many potential releases for each package and the idea is that it's gonna have an id, just like before.<br>
And the release is gonna have a major, minor and build version, which is an integer; and each of these air indexed because we might wanna query by, you know, give me this certain version or all the versions above this version.<br>
Again, when was the release created.<br>
There's a comment on the release, a URL for the release, a size in bytes of the release.<br>
And then, it gets interesting because we have a relationship, we have a package_id, which actually represents a foreign key back to "packages.id".<br>
So when you create these relationships, you've gotta be really careful.<br>
I find myself always just constantly getting tripped up by this.<br>
When you say this line, you talk about the package_id and it's relationship, it's key.<br>
You speak database terms.<br>
The database thing that is related is the lower case, plural, "packages".<br>
That's the table name that has a thing id.<br>
But then we can also use this relationship to get an object that's lazy loaded through the ORM.<br>
When you speak in that thing, you speak in terms of the object that happens to match that table.<br>
So here it's capital "P", singular "Package" class.<br>
Here it's lower case "p", "packages", the table.<br>
Now once you get this down, no problem.<br>
But there it is.<br>
We're gonna create this foreign key, one-to-many relationship.<br>
If you look at package, it's similar but slightly more complicated, id, created_date, last_updated, summary, description, home_page, docs, package_url.<br>
All the stuff that we happen to have shown in the package details page, author_name, author_email, the ur..., the license, excuse me, like MIT or whatever.<br>
And then, we have this relationship that is really quite an interesting ORM thing right here.<br>
So the, this is the one-to-many.<br>
One package has many releases.<br>
Then we're gonna create a relationship over to the "Release" class and it back populates "package", which is that thing right there.<br>
So if I get one, I don't have to do another query to get the other, the reverse relationship.<br>
What's interesting is you can do an order by, not just an order by, but an order by composite key.<br>
So show me the largest version by first, order by the major version and then the highest minor version and then the highest build version among those.<br>
So I think that's pretty unique, you don't see that too often, but really helpful.<br>
It'll let us easily get the latest to the oldest releases automatically just by touching this relationship, we don't have to do anything.<br>
All right.<br>
Moment of truth.<br>
Let's see if I put everything together right here and if so, our Beekeeper Studio, when we connect will not just have users, but it'll also have a package and a release.<br>
It looks like it might, let's go connect again.<br>
Look at that.<br>
We have packages, again empty, but we'll fill it up a minute.<br>
There's all the interesting elements and the releases right there.<br>
Super cool.<br>
You can't really see the relationships.<br>
I don't know that there's like a visualizer diagram type thing.<br>
Maybe there is, I don't know where it is in Beekeeper Studio, there is in PyCharm but sadly, it doesn't work in the version that I have.<br>
We've got our database all set up.<br>
It just has no data.<br>
Wouldn't it be cool if it had some data?<br>
Well, we'll get to that in just a moment, but these three classes are now SQLAlchemy classes.<br>
Let's just review real quick, ideas.<br>
They have to derive from SqlAlchemyBase, the elements get specified as columns and notice one other thing I hadn't, didn't comment on before.<br>
Let's just do it for the users.<br>
Some editors are smart enough to know.<br>
Yes, you said this is a column, but really, really, it says, it's an integer type column, so let's treat it as an integer.<br>
And let's treat this one like a string and this one like a datetime.<br>
Now, not all the editors are smart enough to do that.<br>
So let's go ahead and specify some type information here.<br>
We'll say, that's an int and that's a string and so on.<br>
That's a string, that's a string.<br>
These are datetime.datetime's.<br>
So if I come down here and I say "u" equals "User" and I say u.created_date, look at all the datetime stuff, perfect.<br>
It knows, right?<br>
If I go to "email.", look at all the string stuff, it knows, perfect.<br>
So this just takes it up a little bit of a notch to say when you're working with the class in memory, treat it like the native Python types that they ultimately will get out of the database We've created a class, derive from SqlAlchemyBase, specified the columns and their features like indexes and uniqueness, and then importantly, we put them into this so that they got imported.<br>
They got seen by the SqlAlchemyBase over here, on this line, right before we called create_all.<br>
So it's that thing that looks at all the classes and then creates the corresponding tables.<br>
And that's why our Beekeeper Studio, nice and cool, like it does over here with the tables.
|
|
show
|
4:49 |
We've seen we created the tables, but we haven't put any data in there, and where are we going to get the data?<br>
Anyway, wouldn't it be nice to have somewhat realistic data?<br>
The data we used before was super fake.<br>
We just generated some integers and said there's this many things.<br>
We put like, this is the summary, instead of actual summary information.<br>
Well, you're in luck because I've given, I've gotten and given to you the actual data from PyPI.<br>
So you're familiar with this section, the code all over here.<br>
But now, let's have a look over here.<br>
When I grabbed all this data, I got the top 100 packages and all of their information, and I saved them to a bunch of JSON files, so check this out.<br>
We go in here, we've got ampq, we've got boto, we've got beautifulsoup, we've got the awscli and so on.<br>
And if I quick-view these, you can see they've got an author and emails and classifiers like their licenses and notice, they're not simple.<br>
Like I want to say this is a BSD license but I've got to go to the info, to the classifier to get the license and split it into pieces and find the BSD part.<br>
Say, oh, it has a BSD got it, right.<br>
So there's gonna be some work to read this data, but it's no problem.<br>
Here is the home page, here's the license written on a better way, here's the summary and so on.<br>
So what I want to do is read all of this data into the database.<br>
So I'm gonna go over here and create a folder called bin and This is where I put all these little utilities that like, load up the database or run little migrations or maintenance scripts and so on.<br>
I'm going to drop into here something called load_data because it is not at all worthwhile for you to watch me pulling this all together, OK?<br>
So we're gonna use a couple of libraries we haven't talked about yet.<br>
This really cool library called progressbar2 that does a progress bar while stuff is happening in the terminal.<br>
That's great, unfortunately, it named itself progressbar, and the package is called progressbar2.<br>
Not, not great in that regard but we've gotta put progressbar2 there and we install that requirement.<br>
And over here, this will go away in a second, perfect.<br>
Similarly annoying, we wanna work with thing that's fantastic called dateutil.parser, but it comes from Python-dateutil so the names don't give you a lot of guidance on, you know, what the underlying package to install is.<br>
But I'm walking you through it.<br>
It's all good.<br>
Some of these we're not actually using here.<br>
And this is release and user, OK.<br>
so what we're gonna do is we're gonna go through, I'll just clean this up so it runs fine and all that, you don't have to watch.<br>
What we're gonna do is load up all of those files, those JSON files, and we're gonna parse through them to find out all the users that are mentioned within those and then we're gonna create corresponding users to the ones we discovered.<br>
And then we're gonna go create the packages and then associate the users with their package, who created what.<br>
We're gonna do language imports.<br>
And we're gonna do some license imports and we'll just print out what happened.<br>
So we're gonna run that real quick, and then we'll have data in our database.<br>
All right, that thing I dropped in here actually was importing a little more data that we needed, so we're not gonna worry about it like the language and maintainers, we didn't create the classes in SQLAlchemy before, so there's nowhere for them to go.<br>
So what I'm gonna do is I'm gonna run this, it'll initialize that connection, create one of these sessions, and it'll check, has data already been imported?<br>
If there's no data whatsoever, it's gonna go do the import, then print out a little summary.<br>
Let's run it.<br>
You'll see down to the bottom the little progress bar thing going across, which is cool.<br>
If I press, this, it's gonna run the website, which is not what we want.<br>
We wanna go here, right click, run.<br>
It goes, you can see a little progress bar zipping by at the bottom.<br>
Finally, we have 84 users, 96 packages and 5400 releases.<br>
I know said there was a hundred but I think for some reason, a couple couldn't be downloaded or something like that.<br>
All right, so now if we go back and we connect again over here, we click around.<br>
Look at that, all of our data is here.<br>
Super cool, right?<br>
So we've got our users.<br>
Here's Armin Ronacher from Flask, he doesn't have a password apparently.<br>
Kenneth Reitz, say from requests.<br>
You should know these names.<br>
Here's Ned Batchelder.<br>
Probably because of coverage.py and so on.<br>
Django Software Foundation apparently maintains Django, but these are all the really popular projects right here.<br>
Celery Project, right.<br>
That's expected from the top 100 packages off of PyPI, right?<br>
So here we go, look at this.<br>
We have data, we've got our releases and they go back to their packages, we can see that over here somewhere.<br>
Notice we're using the actual string name of the package as the id, which is pretty cool.<br>
argparse, asyncio and so on.<br>
We're in a good spot, we've got our database all put together, and we've got our collection set all ready to go and actually view these in the website.<br>
But remember, we still have to write the queries to do it.<br>
At least we have the data to write the queries for.
|
|
show
|
7:11 |
It's time to start using our real data.<br>
After all, we have a database, we have a database model, we have the database full of data from actual real live PyPI data.<br>
Let's start using that.<br>
In order to do that, we need to actually do queries against the database instead of having this fake data.<br>
Now the way I like to approach this is sort of a top down mode.<br>
Let's look at this homepage and see what queries we might need to rewrite in order to make that using this live data instead of this fake data.<br>
But just to remind you, Let's click here, this is the home page.<br>
And the most important things to see are how many projects are there, releases, users and then show me the latest releases to the projects that have recent releases Okay, so we want to go and basically do that.<br>
Let's just start here at this index method.<br>
We'll go to the IndexViewModel, hold down command or control on Windows, and you can use these hyperlinks, so I'll jump to that.<br>
Here we have our package_service, which has given us a release count and a package count and our user service, which has given us that.<br>
Let's do the three counts real quick.<br>
I'll jump over to the package_service release_count.<br>
That looks real, doesn't it?<br>
No, no, it doesn't.<br>
So we're going to need to create a unit of work, a session in SQLAlchemy's nomenclature.<br>
So I'm gonna do, from data import db_session.<br>
That is the thing that creates the units of work.<br>
And it's easiest if we do this kind of stuff within a try, finally.<br>
And then we close up the session, clean it up there at the end right away.<br>
So what we'll do, is we're gonna say session = db_session.create_session().<br>
And remind yourself this returns a Session object.<br>
By calling the factory, turns off some of the, force you to reload it too frequently in my opinion, types of behaviors.<br>
So over here we're gonna have the session.<br>
And then instead of this, we're gonna write a query right here.<br>
Do this in line, it's really simple.<br>
So, say query, what do we want to query?<br>
We wanna query the release, to which you wanna go to the table, to the query and just run a count against all of it.<br>
And here we'll say session.close() like that.<br>
Let's see if we got this working, just before we go any further.<br>
Let's go ahead and run it.<br>
Now, we should have, what was it?<br>
5,400.<br>
If we look over here without refreshing, there's some huge number of releases to simulate the real PyPI but our fake data is just a couple hundred.<br>
Here we go, look at that!<br>
Perfect, 5,400 out of the database, that's what it's gonna look super duper similar down here, only one word changes, we want to change that to package.<br>
That's it, we re run, we refresh.<br>
That should go to 96.<br>
It did, and then users.<br>
I think we had 83 users.<br>
Let's go down and write really similar code for the users, but that's over, work our way back.<br>
That's right here.<br>
They're having this fake thing, we'll do that.<br>
Gotta import this, and this is gonna be user, just like so.<br>
That should be the three count queries that we have for our little bar.<br>
Look at that, it's all live data.<br>
How incredibly easy was that?<br>
That's awesome.<br>
That's why we're using an ORM, because once we get it all set, it's incredibly easy to do.<br>
The last thing to do here is to get the latest releases.<br>
So we're gonna go over to our package service.<br>
This may be the most complicated query we're doing in the entire application.<br>
So what I wanna do is say releases, we're gonna start by getting the releases from the table, the releases, here.<br>
What I wanna do is come over here and say session= db_session.create_session() and then try this.<br>
And finally session.close().<br>
So then we're gonna come to our session, and we'll do a query against the release table.<br>
Remember the releases know which package they're related back to.<br>
In order to do this query, we're going to have to, well because we're closing the session and we don't wanna have a bunch of slow, come back and do extra queries against the database.<br>
We wanna do just a one shot thing, we're gonna do a join.<br>
And the way we do that is we come over here and say options and into the options, we're gonna pass the ORM.<br>
Now, that's not imported yet, but it's going to be.<br>
We have a joinedload, and the way we do the joinedload is we specify the relationship on the thing that we're querying here.<br>
So we wanna that relationship, basically its part of the join, so every time you get a release, get it's related package as one query and let's wrap this around and then we also want to go and do an order by because we want the latest one.<br>
So we're gonna go over here and do an order_by and then what are we gonna put in this?<br>
It's going to be release.created_date.desc(), most recent ones first, and then finally, we only want a certain number here.<br>
Now this could cause a problem.<br>
But there's a way to say limit and then you pass how many items you want to go over.<br>
It just happens to be that word, just the same.<br>
And then finally, we wanna say, you wrap that around, and say .all(), wrap that around.<br>
Okay, so those are the releases, plural.<br>
And what we should be able to do is go to each release and get its package.<br>
So that's what we want back, it's not a list of Release, a list of Package.<br>
So finally, after that's all closed up, we can return.<br>
Just do a little list comprehension, say, for each release, we're gonna get its package for r in releases.<br>
All right, Is that gonna do it?<br>
We're gonna find out, aren't we?<br>
So let's run this and oh, look at that.<br>
It's working.<br>
So we've got 1, 2, 3, 4, 5 packages back.<br>
Let's go and change how many were passing over.<br>
Let's say limit equals seven.<br>
Okay, Lokk at that, we've got gevent and awscli.<br>
Now notice, I told you that this has a possibility of causing some kind of problem because of the way our data structured, if a package releases frequently, it could, and if we ask for too many of them, it could show up here, but we can go, maybe be a little bit safe, I guess.<br>
Let's try to do this.<br>
Let's go to package service.<br>
Say I wanna do a set comprehension here instead of a list and then we're gonna turn it into a list.<br>
What will that do?<br>
Hopefully it will make it a unique, sets always only hold one of any given item, it's, it's a matter of what counts as you, you know, the same item for these.<br>
I think this'll' work, we're gonna find out.<br>
Here we go.<br>
We should have 6.<br>
1, 2, 3, 4, 5, 6 because one was duplicated.<br>
So in this case, maybe we query for twice as many as we need and then do this reduction and a limit.<br>
But it's just kind of messed up with the data that we have that it's, there's a chance of getting a duplicate.<br>
But nonetheless, let's just take a moment and appreciate what we've got here.<br>
This is real data coming out of our database that we populated with real data coming from PyPI.<br>
This whole section here is completely live.
|
|
show
|
2:53 |
Well we've got this page working really well out of the database.<br>
Let's click on this.<br>
Oh, well, maybe that one is not working yet.<br>
The project details.<br>
So what we've got to do is, we've got to go and actually get this thing to work.<br>
So let's just jump in here real quick and see what the problem is.<br>
It said, oh, you passed a bunch of arguments, that doesn't really work for SQLAlchemy anymore.<br>
So we could really quickly fix this by saying some of these values, just, if you specify the key names we'd be good to go.<br>
This is gonna be description, this'll be home_page url, we've got the license, and this'll be the author_name.<br>
All right, so that should make it run.<br>
I guess the same problem is true for releases, created_date, OK.<br>
Oh, yeah.<br>
We've also got to pull out the version here, so let's just do a little f-string.<br>
Really, I just don't want to write that same thing a lot.<br>
So let's just say f"{r.major_version}.{r.minor_version}.{r.build_version}", All right, so that's the same thing we're trying to get out there.<br>
Okay, so we've got it working, sort of, didn't we?<br>
This is live, this is live.<br>
All this stuff is fake.<br>
All of those things are fake.<br>
So actually, this may also be fake, right here.<br>
I'm not entirely sure, but definitely everything else is fake here.<br>
So what we need to do is write the queries to make this page go, don't we?<br>
So let's just go through like we did before.<br>
Go through the view model and say were we're getting the stuff.<br>
So the package name is being passed over, that's great.<br>
And it's get_package_by_id Yeah, this is the one we "fixed".<br>
We made it run, but this is certainly not what we're wanting to do.<br>
Let me just grab this real quick here because it's gonna give us a lot of what we need to work with.<br>
We're gonna go over here and do a query against a Package.<br>
I don't know why I grabbed the most complicated one.<br>
We'll do a query against the package, and what we've got to say is filter and, what do we want?<br>
the id is the package_name so we'll say Package.id == package_name, then first().<br>
Just the one, we'll return package.<br>
Perfect, let's see what's gonna happen here.<br>
It's gonna work?<br>
Oh, yes.<br>
So let's just see.<br>
this is working.<br>
Check this out.<br>
So we've got this whole thing coming back and working.<br>
You ca see this is the latest version.<br>
The release was that, I guess, is probably when we inserted it.<br>
Let's go back over here, click on gevent, perfect.<br>
Got our gevent, all the details, were not actually converting the, cause that's Markdown, that's restructured text, were not rendering the restructured text as HTML, kind of a pain.<br>
If we did, then you know, would come out great, but see the license is MIT.<br>
If we click in the Homepage, it really takes us over to gevent, where they probably need an SSL certificate.<br>
But nonetheless, this thing is working great, right?<br>
So we've got our home page, and now we've got our details page working as well.
|
|
show
|
1:34 |
I almost overlooked something, really quickly before we move on from the Package Details Page.<br>
Notice we were just sending this fake release back.<br>
Let's actually rewrite this to return the real package release.<br>
Let's go down here, we'll say, create a session and we want to get the latest release, right?<br>
And I guess there's a couple ways we could do it, we could get the package and then we could get the releases.<br>
Or we could just do a query where the package name is something and order by release date, let's go with that one.<br>
So we'll go to the release and we'll say filter where Release.package_id is package_name.<br>
That should still be set as that foreign key that we're working with.<br>
And then we have the first, that's fine, but we're also going to have, come over here, and we want to say order_by(Release.created_date.desc()).<br>
So we want all the releases for this package order by, going the newest ones first and then just give us that.<br>
And that is going to be "release".<br>
It could be, there's a release or there might not be a release, it might be None, hence the Optional return value here.<br>
Let's try this one more time, make sure we're really using all the live data over there.<br>
If we go to botocore, oh yeah, look at that.<br>
It's got old snapshot data, but there it is, botocore updates all the time so it's a little out of date, but check that out.<br>
We've got the latest release.<br>
We come back to awscli, 05-31.<br>
Ah let's good, to kombu, 05-30 and so on.<br>
Pretty cool.<br>
Now we've got that last query written and working beautifully.
|
|
show
|
7:28 |
Package details is working, home page is working.<br>
There's one final section that has important stuff going on like register, that's not right anymore.<br>
But if I click on register, remember, we're just doing that fake data.<br>
We're setting the cookie, so it looks like something happened.<br>
But there's really no persistence or storage of the users.<br>
Similarly, when we try to log in, there's no checking that there's actually a user.<br>
So we need to write these two queries here.<br>
We've already got the user_count, let's do this create_account.<br>
So I'm gonna go ahead and nab this code cause it's real similar and what we need to do, that is just a little bit different, is instead of returning a query, we need to create an object, insert it in the database, commit those changes and then return that object.<br>
So but, do like this and say the user is a User, and then we'll just set some of its values, id we don't have to set, that's auto incremented email can be email.<br>
Over here, we're gonna set the name, it's gonna be their personal name or whatever, created_date has a default, will get set by SQLAlchemy, hashed_password is, just gonna set this to "TBD" because we're gonna do a little bit of work to do that right.<br>
last_login is set by an auto, by a default, created_date is set by a default, profile_image_url is just gonna be None.<br>
And that's all we gotta say.<br>
So the last thing to do is say session.add(user), session.commit() and then return our new user.<br>
Cool, huh?<br>
So we're gonna use this later, we're not using it yet.<br>
We're gonna come back for a second pass and store the password here.<br>
So this is our query, our create_user and then lets do, we really need something more like this.<br>
Let's go and do the query user as well and maybe I'll just put "# TODO, set proper password".<br>
And while we're here, let's go and do this one as well.<br>
Everyone having the password "abc", that's not the best.<br>
What we wanna do is come over here and say I would like to get a user.<br>
Now, a naive query would be give me the user where the email is this and the password is that but you never, never wanna do that because that means you'll be storing raw passwords.<br>
Don't wanna do that.<br>
What we wanna do instead is get the email and then check that the re encrypted version of the hashed password match, the plain text password matches what the hashed password is.<br>
So we'll do our filter where user.email is the email, I'll say first(), this will be user, here we go.<br>
We're gonna say, give me the user.<br>
If there's no user, gonna return whatever that nothing is, it's very, very, very likely None.<br>
And I'm gonna put just "# TODO: verify password".<br>
We're gonna leave that for a little bit later, not do that yet, and then we're gonna return the user, right?<br>
Assuming that the password, say if.<br>
So if for some reason the password is wrong or whatever, we're not gonna return that.<br>
Okay, so we'll return the user we got back from the database and let's see if we can do this round trip here.<br>
So we're gonna run, down to beekeeper, and I'm gonna go over the users table and let's go and order by created_date, doesn't matter, they're all created right then, but it will in a minute.<br>
We're gonna have something new in there.<br>
So what we wanna do is we want to go over and try to register.<br>
So to register, I'm gonna put my name this time, this is the password, put my fancy letters here, doesn't really matter, but I think it's required.<br>
All right, moment of truth.<br>
This should now go save it in the database, it did not.<br>
Something went wrong, right here.<br>
Ah, yes.<br>
Our AccountViewModel needs to be calling the database here.<br>
Yeah, so what we need to do is, if we look at the ViewModelBase, we've already got the is_logged_in and let's, let's go and set this like so.<br>
Let's set this here, The logged in is that is not None.<br>
All right, so if there is some kind of id set, then they're logged in.<br>
Okay, so we already have this user_id.<br>
And what we need to do is just go and write one more function, is user_service.get_user_by_id, and it would be self.user_id Didn't exist yet, let's go write that real quick.<br>
And this is an int in this case, and it's gonna return like before an Optional[User].<br>
See, there's a lot of similarities going on here.<br>
Copy, copy.<br>
This one's simpler but quite similar in structure.<br>
So we try to get it and, this time where the id is the user_id and let say return that, but in fact, we could inline this and just return that result, doesn't really matter if we look at it.<br>
Sometimes I find having a variable is nice for debugging.<br>
You can say, well, what came back here?<br>
Alright, go and return that but this is gonna be straightforward.<br>
All right, I come over here, try and it thinks we're logged in, but let's go and log out.<br>
Let's go register, again.<br>
Say Michael, email address, five a's and some number, but let's just verify what's happening.<br>
The register actually worked, I believe.<br>
But then, we didn't get the right data back, right?<br>
The error was in the account page, not this, so hit register, and we still have an error, what's going on here.<br>
Ah we're not checking, apparently, we tried to register with the same email address.<br>
We already have this email, so let's go and add, looks like we're missing something in our registration view model.<br>
Yeah, and we didn't do the test, right?<br>
So another test we need to do, say elif user_service find user, user what do we get?<br>
It is get user by.<br>
Let's say get by email self.email.<br>
If you're gonna have a uniqueness constraint, you can only have one of them.<br>
Okay, so maybe you should log in.<br>
Alright, so let's try that real quick, and it's extremely similar to this.<br>
Except for that is email, and that is email.<br>
And that's a string.<br>
Okay, let's go try to submit that again.<br>
Now it comes back, it goes no, no, no you can't do that.<br>
That email is already taken.<br>
Oh, well, let's go and try to log in, we're just not checking the passwords so I'm gonna put only three a's this time, not five.<br>
Remember we're coming back to do that authentication stuff.<br>
Look at that, I've logged in, I'm logged into our account.<br>
Let's try one more time, I'll log out.<br>
I'm gonna log in.<br>
Beautiful.<br>
All right, awesome.<br>
So we've got our account set up.<br>
Let's just go finally, look in the database, we'll do a query select * from users where users.email is my email.<br>
We run it and check it out, there we go.<br>
Name is Michael Kennedy.<br>
password is not set yet, created_date is right now today, profile image not set, but we created our user and we're using it to log in and log out.<br>
Let's check one more thing.<br>
Let's try to log in using a non existing thing.<br>
Sorry, that account doesn't exist in our database.<br>
Super cool.<br>
So I would say we have these queries, all working.<br>
We just needed a little bit extra back here to make sure we didn't create the same user twice, which the database won't allow.<br>
That's it.<br>
We are 100% using our database and writing all the SQLAlchemy queries against our models to make this entire website run.<br>
Beautiful
|
|
show
|
6:46 |
The final thing to get our SQLAlchemy stuff really working 100% has to do with storing passwords.<br>
Now storing passwords is tricky.<br>
You wanna make sure you get it right and doing it yourself is probably not the right way.<br>
So over here, let's check out our TODO's that are pending, says set the proper password.<br>
What that really meant is take the plain text passwords and set the hashed password to a hashed version.<br>
If we just do an MD5 hash, there's these look up tables.<br>
This hash equals this word, this hash equals this word.<br>
We can't do it that way.<br>
Also, we wanna make it computationally expensive to guess if for some reason our database got stolen and somebody got ahold of those hashed passwords, we don't wanna just hash it once, because then you could just guess a bunch of things.<br>
So there's a lot of different things, like bringing salt that is arbitrary text into, mix it in with the word but also hash folding or password folding, where you hash it, and then you take that result and you hash it again, you take that result and hash it again a 100,000 times so that it's really computationally expensive to take a guess at a given word, much more so than a single hash.<br>
So what we're gonna do is we're gonna use this package that makes it much easier called Passlib.<br>
It does all those things with a variety of different cryptographic algorithms really, really easily.<br>
So what we're gonna do is we're just gonna go and add passlib and let it do all the hard work for us and get things right.<br>
So passlib another one to install and it's not misspelled.<br>
Now up here at the top, I need to import something from passlib.<br>
So I'll say from passlib, and I want to import it as a certain thing so if we change the algorithm, we can actually change the underlying algorithm, we don't have to change the code just because the name of the algorithm changes.<br>
So what I'm gonna write is from passlib.handlers.sha2_crypt and here on import sha512_crypt Not like that though, but as crypto.<br>
That way, if we decide to use bcrypt or a different algorithm, it'll still just be one of these providers, one of these handlers called crypto.<br>
Okay, so that allows us little flexibility.<br>
And down here, this is incredibly easy to use.<br>
All we have to do is go to crypto and say encrypt.<br>
We give it the password, and it also takes a bunch of arguments.<br>
See if it'll have documentation inside.<br>
Oh, it looks like it was renamed to hash.<br>
Okay, let's call hash, then.<br>
Fine.<br>
And the secret is password.<br>
Does it say what its keyword arguments are?<br>
I know which one I want, but I'm not sure, I was hoping for documentation.<br>
So what we could do is we could come over and we could say round equals let's say, 172,434.<br>
So I talked about that folding and do a hash, and you do the hash of the hash, the hash of the hash of the hash.<br>
This will do that iteratively 172,434 times making it computationally expensive.<br>
If you wanted to be, take longer, you make it higher.<br>
You wanna make it a little bit quicker, I'm guessing This is about 1/10 of a second, I'm not 100% sure.<br>
Let's go and do this and register a new user and see what gets into our database.<br>
So register, this'll be Sarah Jones.<br>
this'll be sj@gmail.com.<br>
She loves the password "abaaaa" and who knows how old she is.<br>
We hit this, we get our account and let's go to our database and have a quick look.<br>
She's at the bottom and now check this out.<br>
Copy this and I'll just put over this query page here so you can see it Look how giant, enormous that is for, like, four a's and a b.<br>
This is all sorts of craziness.<br>
So it tells you the algorithm that was used, the rounds here, ups don't move that around for me.<br>
The rounds that were used, the number of iterations and then a combination of the encrypted result, along with hash that was randomly created for the salt that was randomly created for it, and so on.<br>
So this is way more secure than storing plain text.<br>
Even better than just hashing it once, right?<br>
So it has randomized, per user salt, plus the iteration.<br>
Really, really nice and how hard was it?<br>
It literally couldn't be easier.<br>
crypto.hash(password).<br>
Now, what is not so easy is how do I verify that?<br>
How do I verify that that is actually the password.<br>
Well, what we're gonna do is I'm gonna come over here, we don't have to remember the rounds, it's stored in the result.<br>
But when we log in, we need to say we're gonna get the password that they provided and we wanna compare that.<br>
So we're gonna come over here and say crypto.verify.<br>
It doesn't say "decrypt" because you cannot decrypt the hash.<br>
But you can look at all the pieces and hash it again and say, Did the outcome match?<br>
So here what we're gonna pass over is we're gonna pass over the secret, which is the plain text password And then we're gonna pass over the hash, and that is what we stored on the user object, the hash_password.<br>
And if it's not verified, We're gonna say no, get out of here.<br>
Otherwise it will return the user.<br>
So look at that, to use passlib, literally that line and that line and we're covering so many good practices around storing passwords, incredible.<br>
Let's go try to create, let's go try to log in, as our other user.<br>
Hope I remember the password, right?<br>
It was sj@gmail.com, I think it was "abaaa" We'll find out if it doesn't work.<br>
No, password's wrong.<br>
Let's register a new account.<br>
There's no reset.<br>
Sarah Jones2 sj2@gmail.com and we'll set a decent password and register.<br>
Now let's log out and see if she can log in again.<br>
So come over here.<br>
Use that same email address.<br>
Boom, we're logged in.<br>
That's awesome.<br>
If I were to put in an invalid password, something else, nope, doesn't exist.<br>
It doesn't let us log in.<br>
So really, really fantastic.<br>
This passlib, how easy it makes storing passwords and doing user management correctly.<br>
FastAPI has other ways of authenticating users and doing authentication.<br>
We're not gonna go into it in this course, right?<br>
Use just this user name and password integration here.<br>
But if you wanted to do like Federated Identity or OAuth or stuff like that, the framework does support it, but it's sort of beyond the scope of what we're doing here to get deep into user management and different types of authentication.<br>
Pretty awesome.<br>
I'm super, super pleased the way this came out, that is the way to do things.<br>
And that's a really nice way to verify that they are logged in correctly.<br>
You saw in the database, go back to our users.<br>
Now we'll have a couple examples.<br>
You look at these two.<br>
The number is the same there, but then after that, it's just all randomness, and it's not anywhere near the same thing.<br>
Super super cool way to store this users in our database.
|
|
|
39:07 |
|
show
|
1:31 |
Welcome to the async SQLAlchemy chapter.<br>
I'm so excited to finally be presenting this part of the course to you.<br>
We've been building towards this for a long, long time, and I'm so excited because this is how we're going to take advantage of one of the most powerful features for creating truly high performance web applications with Python.<br>
Async and await, one of the reasons I love FastAPI so much is it makes it really, really simple and straightforward to create async web applications, you simply use the cool async and await keywords that come with Python, and it handles the rest at the infrastructure level.<br>
We can have async and non async code mixed together in different view methods or different API methods, and it just knows how to treat one as async and one as a regular web method, but in order to take advantage of that, we have to have async for all the external systems we're waiting on.<br>
If we're calling an API, we need to use some kind of client that understands how to do that asynchronously and importantly, one of the biggest things, the biggest systems we wait on in web applications is the database.<br>
In fact, many web applications, I would say, do more processing or computation, spend more of their time in the data base layer than they do in the actual web layer And with async and await we can take all that time we spend in the database and just completely free it up to do way more web processing.<br>
So this is really, really a key piece of the advantage of FastAPI and we're gonna dig into it now.<br>
It's gonna be awesome.
|
|
show
|
1:45 |
I need to put a warning right in front of the beginning of this chapter.<br>
We're using the async functionality of SQLAlchemy to unlock the database async behaviors in FastAPI.<br>
That's awesome, SQLAlchemy is by far the most popular ORM if you are excluding Django ORM, which is tied to Django, of course.<br>
So it makes sense to use SQLAlchemy.<br>
However, as we already saw when we started the previous chapter about the synchronous version, the async support is in flux, and we only have a beta version of the API to work with.<br>
Now beta, that's not alfa, that's not pre-release, that's somewhat stable, right?<br>
But at the same time, it's not final.<br>
It's, there's no guarantees, and it's only a step towards what they're calling the 2.0 version of SQLAlchemy.<br>
So for that reason, we're going to have to use the beta version of SQLAlchemy.<br>
There may be some changes along the way, so I wanted to point out that I've created this course revisions document just at the top level of the github repository called revisions.md and my goal is to put any changes like you need to make this small code change, or warning, we had to change this thing about the way we were working with any of the code in the course.<br>
But the one that I think we're gonna end up potentially making changes or having any issues around is gonna be this part of SQLAlchemy.<br>
Be sure to check this document.<br>
Right now, you can see it says initial commit no changes, and that's because it's just what we're recording.<br>
But over time, there might be some issues we run into.<br>
I'll update the code in the repository.<br>
Maybe you run into something.<br>
Create a github issue and let me know in the repository, and we'll keep this working will keep it fresh.<br>
But in order to do this now, instead of waiting months or maybe years in order for the support to be fully there we're gonna have to use the beta version.<br>
Who knows what's gonna happen in the future so just a heads up on that.
|
|
show
|
4:52 |
Before we dive into writing code around async and await and moving that up into the FastAPI layer, I wanna just broadly cover what is the value and where does async and await play a role in scalability.<br>
Now scalability can mean a lot of different things to different people.<br>
But the idea of scalability is not to make one thing faster, it is to allow you to do more of the same thing without slowing down, right?<br>
So the goal here is if we have, say, five requests coming to our site, and then there's some kind of burst and we get 500 requests coming to the same website.<br>
We want it to be able to handle those 500 requests at about the same speed as it would handle the five.<br>
That's the goal.<br>
That's the scalability that we're looking for.<br>
We're gonna examine two views of a theoretical web server doing some processing, one that is a traditional Python WSGI, Web Service Gateway Interface application that does not support async view methods.<br>
Traditionally, this has been Django, Flask, Pyramid, all those different frameworks.<br>
Django is starting to add some async features, Flask is thinking about it.<br>
Maybe by the time you watch this recording, they've actually made some progress there.<br>
But many of the popular Web frameworks do not support this async scalability.<br>
So this first view that we're gonna look at, this is, this is, you know, traditional Flask.<br>
Let's say at least at the current time of the recording, and we'll see that we're going to, as we get more work sent to the server it's just going to take longer and longer because, well, it's not as scalable.<br>
So let's look at a synchronous execution here, and we're gonna get three requests.<br>
Come in really quickly.<br>
Now, these green bars are meant to represent how long it actually takes us to process from the request coming in to the response coming out for the page that Request 1 is pointing at, and the page that Request 2 is pointing out and see Request 3 down there at the bottom, even though it came in third, it's actually incredibly short, so it should come out really, really quickly if there was no other traffic.<br>
But what happens in this synchronous world?<br>
Well, Request 1 comes in and boom, we're gonna start processing it right away.<br>
How long does it take from a user's perspective from actually the request hitting the server until the response goes out?<br>
Exactly The same amount of time is as it would normally take, right?<br>
It's just that's how long it takes.<br>
But when the Request 2 comes in, it has to wait to begin processing.<br>
It cannot begin processing its response until response one is done, right?<br>
Because the server is synchronously working, it can't do more than one thing at a time, so it's going to wait to get started.<br>
And the big yellow bar is how long.<br>
It seems like the page takes to load to the user who made the request because they've gotta wait for this Request 1 they don't know about, plus the time it takes for theirs.<br>
Poor old Request 3 comes in just after Request 2 and it takes as long as that big yellow bar for it.<br>
It's gonna take a really long time for it to get the response.<br>
If it had been processed alone, it would be really rapid, just the size of the green Request 3 bar, but because it has to wait for 1 to finish and for 2, which is waiting a while for 1 and then itself.<br>
It doesn't actually get processed for a long time.<br>
So for the user, from the outside, it appears that this Request 3 takes the big long yellow bar time to get done, even though it would actually be really quick.<br>
What's going on here?<br>
Well, if you actually dig into one of these requests, let's say Request 2, for example or Request 1, doesn't really matter.<br>
One of the longer ones.<br>
It's not just processing, there's a bunch of systems working together in web applications, right?<br>
Maybe we're calling an external API through microservices, maybe we're talking to the database and so on.<br>
So let's expand this out.<br>
What does this processing actually mean?<br>
So here's Request 1 coming in, you could think of it as just we're processing the request, but if we break it down into its component pieces, it's a little more interesting.<br>
We have the framework that we're doing some database work and then a little bit of our code runs, which issues another query, which takes a long time over to the database.<br>
Get a response back, we do a quick test on that, and then we send that off to a response in the framework.<br>
So how much work are we actually doing?<br>
Well, maybe the gray plus the blue, the gray plus the blue is all that really has to happen.<br>
This database work, this is a whole another computer, another program that we're just waiting on.<br>
So there's no way in a synchronous web request to allow us, our website, our web framework, our web application to do other things while we're waiting, we just call the query and it stops.<br>
That's not great.<br>
This is why there's such a blockade when Request 1, 2 and 3 are coming into the system because we're just waiting on something else when in fact we're only doing a small amount of work.<br>
Maybe a 10 to 20% of this request is actually us doing the work where the server would be busy.<br>
Most of it is waiting and async and await in Python are almost entirely about finding places where you wait on something else and allowing other things to happen while you're waiting.<br>
So there's a huge opportunity to do something better right here.
|
|
show
|
1:42 |
If we live in an asynchronous world like FastAPI allows us to do incredible easily.<br>
This picture looks much, much better.<br>
So here we have three requests coming in.<br>
Look at this picture now.<br>
So Request 1 is gonna come in and guess what it's gonna take as long as it takes to compute that.<br>
And then we'll get a response back halfway through.<br>
Remember, what we were doing, maybe a third of the way through in Request 1 is we were waiting on the database.<br>
Well, anytime we're waiting, if we're using async and await, we're allowed to just completely go do other work during that time with almost zero overhead in fact.<br>
When this request comes in, we can start on it right away, and we pretty much do a couple database queries and just wait.<br>
So now we have Request 1 and Request 2 in flight, but they're both just awaiting a database response.<br>
Request 3 comes in and boom, we can get the answer right back to them.<br>
How much better experience is this for the user?<br>
Yes, it still takes the same amount of time to that database period while we're waiting for say, Request 1 or Request 2 or even Request 3.<br>
There's no speed up of waiting on that database.<br>
Our initial request is the same speed.<br>
But as the traffic builds, it doesn't slow down the site because most the time we're waiting, we can just keep on doing other stuff, serving Request 3 while we're waiting on 1 and 2, for example.<br>
It's beautiful and we're gonna see how to do this with FastAPI and SQLAlchemy in this chapter.<br>
If we zoom in, again, where we had our framework and our database and then our code, all this stuff that's green, we can now go process other request because we're just waiting on the external database to get back to us.<br>
Really, really nice with just simply converting those queries from a regular query to an async query we're good to go.
|
|
show
|
3:43 |
Here we are in chapter eight now.<br>
You can see I've copied the code, the final code from chapter seven over as per usual, now it's the starter code for chapter eight.<br>
In order for us to work with the new version of SQLAlchemy, the first thing we gotta do is actually we've gotta go over here and adjust this to say, this is a certain version and funny the way this is coming together, we're creating a fake PyPI, we're going to need to go to the real PyPI and find SQLAlchemy, go to its releases, and here's the beta version that we want.<br>
And we can just copy that bit right there.<br>
And so what we wanna put over is this.<br>
That'll allow us to use the async features that are not in the 1.3 edition.<br>
Let that reinstall here.<br>
All right, that looks like that worked.<br>
Now the API is different for working with SQLAlchemy.<br>
We don't create just a session and use the query the same, what we do is we create an async session.<br>
The first thing that we have to make changes to is to our db session class because we need to do the async stuff.<br>
The other difference in the API here, in the traditional one, we created a factory that creates the session.<br>
But what we're going to need to actually store and notice if we go down to where we put this together, We've got a connection string, we create an engine, the engine is passed the factory, and then the factory is used later.<br>
In the a async model, what we do is we have to hang on to the engine and then each time, use that to create kind of a context block type thing with the engine.<br>
So we go over here and we're going to create an __async_engine that's just this internal thing, It's gonna be an async engine, which we can import from sqlalchemy.ext.asyncio and we'll make that Optional of that because we want to set it to be None in the beginning, and the way that we create it is pretty similar.<br>
So here we have this create_engine and we're gonna do something like that.<br>
But instead of calling this, we're gonna say, create_async_engine like so, the parameters that go to it are the same but this set here, this async create is what we need.<br>
And it's telling us that this needs to be registered as a global for us to make a change instead of just overriding a local.<br>
Okay, so that's working.<br>
And then this create_session thing is gonna use that over here.<br>
So we'll say def create_async_session.<br>
Let's call it that.<br>
This is gonna return an AsyncSession like so.<br>
We'll say we're gonna use this global __async_engine here.<br>
And let's just do a quick test, it was like this where we said, you know, you've not set this up before, so for some reason, if our __async_engine is not called, then we know that they didn't call the right set up, and this is gonna be a big problem for us, perfect.<br>
And then this is pretty straightforward.<br>
We're gonna create the session, which is an a AsyncSession.<br>
Instead of just calling the factory, we're just going to allocate it and pass the async engine along like this.<br>
And from here on, it's pretty similar to what we have before.<br>
Pretty similar, not the same.<br>
Now notice this expire doesn't, expire_on_commit doesn't exist.<br>
So this is the async session but there's within it async session for which we can set the expire_on_commit.<br>
All right, so that's pretty much the changes that we're going to have to make in order to have our async engine stored and then the ability to create these async sessions.<br>
We're gonna go and use this function instead of this one to go through and do these async queries against SQLAlchemy.<br>
Let's just run it to make sure when it runs this whole startup code, everything's hanging together.<br>
Perfect, it looks like it is.<br>
So our foundation to create these sessions we need to rewrite the queries is all in place, looks good.
|
|
show
|
7:46 |
Now that we have our db session with async capabilities, it's time to go use that for some queries.<br>
And here's where we're gonna get into the new query syntax for SQLAlchemy So we don't need db_session anymore, we're just gonna use it and let's go work on the user_service for now.<br>
There's a couple of queries that we want to go and rewrite.<br>
For example, let's work on the count.<br>
Recall, this is the one that's shown right on the home page when, it's got the little gray bar that says, how many packages, releases and users there are.<br>
So we're gonna do something similar to this, but not exactly the same.<br>
So let me comment this out and leave it here to guide us.<br>
We'll say.<br>
You'd like to say db_session = db_session.create_async_session() but things are a little bit different, as I've indicated here with this async API.<br>
What we're gonna do is we're actually gonna do an asynchronous context block, a with block, but one that's async, that's a little bit funky syntax in Python, but that's how it goes.<br>
So we'd like to say something like this with that as such and such, and then we're gonna use it.<br>
And I, for one, am really happy to see the session being able to be used in a context block, that means it'll clean itself up like you saw where you're kind of simulating that with try, finally.<br>
Well, now, in this new API you can just put it right here.<br>
So this is cool, but because this is asynchronous, what we need to do is do an async with block here, and then we're gonna do a query.<br>
Now, the queries look a little bit different in this new API.<br>
Instead of saying session.query, what we do is we actually create a select statement and then we execute it.<br>
So we'll say, select, I'm gonna import that likely from sqlalchemy.future, and in here, we're going to put some kind of statement.<br>
Now, normally, it's pretty straightforward, like user, and we do like a filter and and so on.<br>
But for count, it's a little bit funky.<br>
So what we're gonna do is, we're gonna say, I'm gonna import this thing called "func", which comes from sqlalchemy, and then there's a way to say count(User.id).<br>
So we're gonna pass a function that's counting the user id over to the select.<br>
Now we're gonna go get the results, because this is just a query that has yet to be executed.<br>
So here's where the session comes into play.<br>
We'll say execute and we give it the query.<br>
This is where we go talk to the database, and when we talk to the database, this is our chance to do other work while we're waiting, right?<br>
If I were to, look at this and then print out the type here, let's just do a, a really quick print type of result and then I'll, let's return 12, how about 42.<br>
That's a good number of users.<br>
So when we run it, you'll see something happen, and then this, and should probably see a warning as well.<br>
So let's run this and okay, so we've gotta do one thing first.<br>
In order to use async or await within a function, we do have to make it async here.<br>
Alright, and in order to actually call it when we're calling an async function, we need to sort of go up the stack and await that as well.<br>
So let's go and do that.<br>
We'll go see where this is used, which is going to be in our home IndexViewModel and notice, already right here PyCharm is saying there's a problem.<br>
What is the problem?<br>
Well, the problem is that we got a coroutine and we wanted an integer Oh, this is an non executed, but could be computed, could be executed asynchronous function.<br>
And in order for us to actually interact with it, what we have to do is we have to await it.<br>
We'd have to type this await here.<br>
That's cool, except for there's a small challenge here.<br>
Constructors cannot be asynchronous, so that means we're gonna need to split this apart into two pieces.<br>
Let me do it like this def load or something like that.<br>
And here, we're going to just make this all equal to zero at the start and then we'll set them to something meaningful.<br>
I'll just use an empty list for now.<br>
Okay, so over here because this is also async, right?<br>
This kind of propagates up the stack, like I said, this is now going to be an async function that we can then call.<br>
Once this one's async we're gonna need to call it so let's go further up the stack here.<br>
Typically, how this goes.<br>
And over here, we didn't previously had to call vm.load() but now we do.<br>
And if I try to run this, you'll see some weird stuff happening.<br>
When I request it.<br>
It worked but there is no data.<br>
And if you go down here, you can see warning, warning, Index, scroll over, load was never awaited.<br>
Okay, so this actually never really started, it never did anything.<br>
So we can await this, that's the warning I was looking to show you.<br>
We just gotta work our way out far enough to get it to actually run, so you can see it.<br>
Now, when we do this in order for us to have that keyword there, this has to be an async function.<br>
And here is where FastAPI is awesome.<br>
What do we have to change in the way we're running the program, like the web server were using, the way we might deploy it, the way that we specify a route or any of those things?<br>
Nothing, nothing changes.<br>
It's all exactly the same, FastAPI just knows that this is an async function and it'll automatically execute it correctly.<br>
So let's run this again.<br>
And now if we go and request that page, you can see we now have our 42 right there.<br>
And somewhere down, there's another warning that there's another coroutine that was not awaited, this execute.<br>
Took me a while to get to be able to show you that, but there it is.<br>
We go down here, this is actually an asynchronous function.<br>
So when we're talking to the database here, we need to tell Python we're about to wait on something external, You can go do other things until this finishes and then please pick up here on.<br>
Okay, So if you look at that print somewhere back here, you can see that this is a coroutine just like we already saw.<br>
But now if we await it, this is actually going to be just a result.<br>
But let's go print it out one more time.<br>
There's, there's something different as well here, result, and let's put the type of result as well.<br>
We'll do a quick refresh, we have an iterator result and also just says that thing.<br>
So if we put, let's put this into a list so you can actually see what comes out of it, not the same shape as it was in the previous API.<br>
So look what we got, we got 87.<br>
That's good, that's the number of users.<br>
But what is this?<br>
That is a tuple.<br>
We got a tuple here.<br>
So the way this works, in the new query syntax in SQLAlchemy, what you get back, our table rows are the value or the object that you tried to query and then maybe other things.<br>
I'm not sure exactly all the different things that might be returning there, but you get a tuple of results.<br>
So in order to actually get the result that we're looking for is we need to go and say, return the scalar, I thnink scalar will do it.<br>
If you go and request that, this should now turn that 42 to an 87 and it does.<br>
Okay, so remember when I said this is probably the API you know, and this is a little bit different.<br>
I'm sure that this looks quite a bit different.<br>
Both because it uses this select thing, you pass in additional functions, you await the execution.<br>
But also because the results you get back are these tuples where one of the elements is what you actually used to get back previously.<br>
Okay, so not really a big deal at all.<br>
It's quite easy.<br>
It's just like I said, not that similar.<br>
So let's go over here.<br>
The next one we gotta write is create_account and then login and then get this two users by id and by email, both are gonna be pretty similar to the one we just wrote.
|
|
show
|
4:24 |
Next up, let's rewrite these two, get_user_by_id and get_user_by_email here.<br>
So I'll comment this one out, and it's gonna be quite similar to what we just did, which I copied.<br>
So we're gonna create this async session, then I'm gonna create a query, but now the query, this one's a little bit more like what you would expect.<br>
I'm gonna say, gonna create a select of user where we filter, I wanna filter by user, what are we doing, id, this time equals user_id.<br>
Now previously what we did, as you can see right below is we did first().<br>
So we're not gonna do that here.<br>
The, we can't do those types of queries directly on the select statement, we do it on the results.<br>
We gotta come in here and get our result, and then we're gonna say scalar_one_or_none.<br>
So give us the actual value, which in this case is gonna be a user object, the one that you got or return None.<br>
Don't crash if there's no results, like if they pass the wrong id or in the case below, we're checking to see if it exists, right?<br>
Or to say, give me this, Is there a user by this email?<br>
Yes or no?<br>
So we could also rewrite this one really quickly as well.<br>
In this case, it's just email and email for our comparison here.<br>
And that should do it.<br>
Maybe format it like this.<br>
Make it look a little better.<br>
All right, so this should do it.<br>
But in order for us to write this code, we have to make this async functions, right?<br>
And then where they're called, we have to convert those things to async, right?<br>
It propagates up the stack.<br>
In this case, we got our AccountViewModel, and it's gonna need one of these def load, which is an async function.<br>
Then here it says, warning, you have not defined a user class in the globals object yet, right at the startup.<br>
So we want to make sure we always do that.<br>
It's going to be an Optional[User], we don't know if there's one coming back or not.<br>
Initially, there won't be one.<br>
Now, PyCharm should be complaining to us here, but it's not so if we go look, this is an async function and where we're calling it, what we get back, as you saw is a coroutine.<br>
So we need to await this right there.<br>
Right, then up the stack.<br>
Where are we using this?<br>
Well, we're using that in our view for account in the index right here.<br>
So we'll await vm.load(), which means this has to be async.<br>
Now we're all the way to the top of the stack.<br>
Hand that off too FastAPI, and it'll go.<br>
Let's just see if this will work real quick.<br>
So let me first log in, There's a problem, not gonna work, is it?<br>
It does work, look at that.<br>
We haven't changed that function yet.<br>
So over here, we got Sarah Jones2, and we were able to log in.<br>
This is our slash account, which is running that code right, ups moved around, running that code right there.<br>
Perfect.<br>
The other one that we were working on is this one right here.<br>
So we need to see where we're calling this.<br>
We're just calling it in this one place where we're doing this check.<br>
And so this one is already async luckily, because we had to do that for the form.<br>
So we're just gonna need to make sure we're awaiting this.<br>
So we wouldn't have been able to register because it would have said coroutine is not none.<br>
So that means the email was taken, not actually true.<br>
Let's run this again and make sure we can register now.<br>
I log out, register and register a third Sarah Jones.<br>
See if she can register.<br>
First let's try to register with her old self.<br>
Nope, that email is taken.<br>
Oh, yeah, that's right.<br>
This is actually my email.<br>
Let's try that.<br>
Boom.<br>
Sarah Jones3 was able to register.<br>
All right, So hopefully you're seeing the pattern here, right?<br>
What we do is deep, deep down in the service layers, we switched to the async session objects, we await the database queries, that converts these methods to be async that just propagates up the chain.<br>
So If we're over here, in the view model, we have to make sure that the load, we have some function that we can make async where we're doing these calls.<br>
For example, this one already was.<br>
We had to make sure we added the await right there to actually get the email back, not just the coroutine
|
|
show
|
2:03 |
Next up is our create_account.<br>
This one is not asynchronous yet.<br>
So let's borrow this little bit here and make it asynchronous.<br>
So we're gonna do that, and that makes this method require async.<br>
We no longer need our try, finally, because we've got our context block already.<br>
And what else do we need to do?<br>
We could actually do this right here, ahead of times outside of this async behavior.<br>
So this will run right away, including this, which is computationally slightly expensive.<br>
So we can do ahead and run that.<br>
And then as soon as we've gotta talk to the database we'll go and await it.<br>
So we begin, basically begin a transaction, add this record to be committed.<br>
This commit here, this is asynchronous, this is where we're talking to the database.<br>
So we need to say await, you can go do other work while we're talking the database.<br>
When it gets back with the result, we'll send it on.<br>
We're gonna return, you could even do it like this, we could return our user.<br>
So everything looks good.<br>
That's pretty straightforward but remember, wherever this was being used, we now have to go and make that async.<br>
So that means this becomes await right there.<br>
This one was already async because we needed that for the parsing of the form, which is a async in and of itself.<br>
All right, we should be able to register again.<br>
But this time, using the async capabilities.<br>
Log out, what do you think?<br>
time for another Sarah Jones register?<br>
How about Sarah Jones4?<br>
And again, let's go ahead and just verify this works, the email is taken.<br>
But if it's 4, you can create the account.<br>
Here we go.<br>
It looks like we created the account so, perfect.<br>
This bit of code seems to be working great.<br>
We can now asynchronously, create users.<br>
Super simple, very similar to before actually, in this one even more.<br>
This is the most similar code you'll see from anything that we've done.<br>
We just, instead of creating the session, adding and calling commit, we created it with the context block and we await the commit.<br>
Easy.
|
|
show
|
3:42 |
The final user method that talks to the database that needs to be converted to this async API is login_user.<br>
Again, pretty straightforward.<br>
So I'll come over here, like this, and we want to get a hold of the user by email so I'll leave some of these tests in here.<br>
This is gonna be different, of course.<br>
We do a query, which is a select of User like this.<br>
And then the filter goes on, but not the first, that goes to another location.<br>
Then we say result equals session.execute(query).<br>
Now, remember, this is the async part right there.<br>
We're talking to the database, so we say await and then finally, we're gonna get the user.<br>
Not by saying first, but results.scalar_one_or_none, that's how we did it.<br>
We check, are they there?<br>
So we get the user back.<br>
If there's no user with that email, well obviously they're not logging in, are they?<br>
But if they do come back, we want to verify their hashed password against rehashing the password.<br>
But if for some reason the password's wrong, also return None.<br>
And then we return the user.<br>
As before we gotta asyncify this and then find where it's used and push that up the stack.<br>
This one awaits it.<br>
And because we're doing the form stuff, those are always async the way that they work.<br>
So that method was already good to go.<br>
Very cool.<br>
Let's go and try.<br>
to log this in here.<br>
Log out, close some other tabs.<br>
Let's go try to log in.<br>
Let's go log in as Sarah here.<br>
Perfect, we logged in as Sarah Jones2.<br>
Let's log out, and how about Sarah Jones4?<br>
Yep, we can log in to Sarah Jones4.<br>
And let's try one more, log in as other@gmail.com, "abc".<br>
Nope, there is no other.<br>
And also, let's make sure, even if we have the right email, but the password is wrong, still can't log in.<br>
Pretty awesome, in fact, because the way async and await works, we have the ability to do a little bit better here.<br>
In this login part, it is somewhat computational here, but you'll find once you get your site on the Internet, after a while, if it's popular, people are going to start hammering away on it for various reasons to just cause mayhem.<br>
Try to guess passwords, all kinds of stuff.<br>
The one thing we could actually do here is we could go and say something kind of like time.sleep(5) And so you know what?<br>
I'm not gonna get back to you for another five seconds to tell you whether or not that guess was right or wrong.<br>
However, if you do time.sleep(5), this is gonna be bad.<br>
It's going to literally lock up the server on this whole thread for everyone.<br>
Doesn't matter if you're using async and await or not.<br>
But if you go to asyncio, import that, there's a sleep right there and this one we can await.<br>
So this one, it's just gonna stash it off in the queue and then pick it back up to run in five seconds.<br>
It won't have much overhead at all.<br>
Let's go and try this one, that is a little more picky if you get the login wrong.<br>
So let's see, are we logged in?<br>
No, let's first see that it logs in super fast, if you get it right.<br>
Bam!<br>
Yes, it does.<br>
Log out, log in maybe one more time.<br>
Even quicker.<br>
Yeah, perfect.<br>
But if for some reason we get it wrong, put in junk for our password, for example 1 one thousand, 2 one thousand, 3 one thousand 4 on e thousand, 5.<br>
Yeah, there you go.<br>
No, that wasn't right.<br>
So if you want to slow people down, if they're doing things like trying to guess at coupon codes, access codes, other pages, accounts, its really easy to asynchronously just put them on the chills so they can't guess nearly as quickly which I think is kind of cool, actually.
|
|
show
|
7:39 |
Over here in the package service, those were the final queries that we needed to convert to async.<br>
And because it's exactly the same as what we've been doing, I decided to go ahead and do the conversion, and we'll just talk through it real quick.<br>
So for the release_count, just like we saw before, we're gonna do a select with a function count of Release.id and execute it, that's pretty straightforward, at least given what we saw for users.<br>
package_count, exactly the same.<br>
But Package instead of Release.<br>
This latest_packages, remember, this was our most complicated query that we had in our system.<br>
But from here to here, it was exactly the same as the old query.<br>
We just have to, instead of doing session.query, we just say "select" and then you can't do the all() anymore, what you do is just scalars() like that.<br>
Then we did this little set trick just like before to make sure we get only unique ones in case a package has had a release within a couple of times of some of the other ones, right?<br>
We had a little duplication, and that gets rid of it.<br>
get_package_by_id again super similar.<br>
You get a select, here your filter just like before, await executing it.<br>
And this first() is now scalar_one_or_none.<br>
Also converting the latest release for a package.<br>
Very, very straightforward.<br>
This was basically the same thing.<br>
Execute it, these are scalar_one_or_none, good to go.<br>
So that's it.<br>
Those are all the queries.<br>
But what I wanted to do together with you, it's just integrate these queries back into the rest of the application.<br>
So let's go find where this is being used and update it.<br>
It should be just that IndexViewModel right there and notice PyCharm is saying all of these, all of these are a problem.<br>
None of them are returning integers or lists, what in fact they're returning is coroutines.<br>
We gotta execute the coroutine and get its value out by awaiting it.<br>
And then this one, we now can say more concretely, this is a list of Package.<br>
That will give us a tiny bit more autocomplete.<br>
So release count, package count, we already just took care of.<br>
latest_packages, I believe this is also just used in that one location.<br>
Yes, it is and we're doing things, I'll get there.<br>
get_package_by_id, where is that being used?<br>
You'll find it, there you go.<br>
It's over in this DetailsViewModel.<br>
Now, this is one of those that's going to take some work.<br>
We now have to await this, and this one is also async, so we're gonna have to await that.<br>
That means we will need to do some sort of work.<br>
Let's reorder this a little bit, like this.<br>
We need to do some sort of work in order to allow us to await this.<br>
Like I said, constructors, the dunder init, cannot be asynchronous.<br>
That was already down there, so I guess that was just duplicate.<br>
So this part right here needs to run somewhere else.<br>
However, these things need to be defined as optional things of whatever they are, so this means Optional[Package], so we can write this code here, define it correctly, and then load it up.<br>
Come over here, same type of thing but for Release.<br>
Package release, the latest version, that's already set up there.<br>
Okay, good.<br>
So now all we gotta do is make an asynchronous function that we can call wherever we were using this before.<br>
Remember, it's called DetailsViewModel, and it's in packages.<br>
So that would mean it's in the package view detail function.<br>
There's only one, apparently, so that kind of solves that right?<br>
But the last thing to do is make this async and await vm.load() we should be good to go with that.<br>
We may have just done it.<br>
That might have been the last function.<br>
Let's see, where is this one being used?<br>
Yeah, it's already being awaited.<br>
That's cool.<br>
Oh, however, I just now noticed, you may have have already noticed that, we need a "self" over here to pass it along, perfect.<br>
All right, let's go click around the site and see if it works.<br>
Make sure there's no "you forgot to await something" warning.<br>
So here we have number of projects, releases, users.<br>
Those all look real.<br>
Notice I added two more by adding those Sarah Jones3 and 4 in, which is pretty cool.<br>
We have the awscli.<br>
The part that we just worked on is, what happens if I click this?<br>
Mmm, not something good.<br>
What has happened here?<br>
Multiple rows were returned, right here.<br>
Oh, yeah, we just want to say scalar I believe.<br>
It's, scalar_one_or_none is if I'm trying to get some kind of like user or something were it's supposed to be unique.<br>
Let's see about that.<br>
There we go.<br>
Because, of course, there's multiple releases.<br>
We just want one, which is the latest.<br>
Alright, perfect.<br>
This is working right here.<br>
Looks like everything's good.<br>
We could check out the home page, that awscli sure looks like it, so that, that's working.<br>
Let's click on one other, gevent, perfect.<br>
There's details about the gevent, the library, and this is all working.<br>
So homepage, package is good.<br>
Let's just one more time.<br>
Log in, log out.<br>
We log in, log out and finally do one more register.<br>
I'm sure this will work because I've done it so many times, but we'll do a test with the wrong one there, already exists.<br>
That's right, we wanted Sarah Jones5, and Sarah Jones5 is now registered.<br>
That's it.<br>
Let's double check, really quick here, make sure there's no "you forgot to await something".<br>
No, no warnings either.<br>
Looks like we got everything converted over to this new SQLAlchemy style of programming.<br>
Now I want you to just think back to that diagram where we saw the animations where we saw all the interactions, so over here.<br>
Here, where we saw those interactions when our request would come in to, like, slash project slash whatever.<br>
What are we doing here?<br>
We're going.<br>
And we're saying I want to start a little bit of work.<br>
So this whole section here is gonna run synchronously, which builds up the variables to work with.<br>
And then we're gonna await this load jumping back over here.<br>
This is where we're going to the database to do a query and say: you know, anything that's happening right now, if another request comes in while we're waiting for the database, cool, just go let that request do its thing, maybe start off another database call, when this database gets back to us, just put the answer right there.<br>
Oh, now we're gonna start another database call, and if anything happens during that, it's cool, just go process it.<br>
When this one's finished, drop the answer right there.<br>
And then a little tiny bit of work, like checking for None on two items and a string format.<br>
Most everything happening in this whole series right here is about waiting on the database, in those two database calls.<br>
Were doing that asynchronously, that means we're not blocking up the server, were not consuming hardly any resources while that's happening, on the web server.<br>
Obviously the database is working its heart out, trying to give us the answers, right?<br>
using its indexes and doing its thing.<br>
But as long as the database can handle the traffic, this particular part of our application is not gonna be the bottleneck, and that is super, super cool.<br>
It's possible because async and await, we were able to use async and await because we're using the latest SQLAlchemy, the beta version that supports actually asynchronously talking to the database.<br>
Super cool, hopefully you'll appreciate this a ton.<br>
I think it's really, really neat, and it unlocks some amazing potential for your web app.
|
|
|
41:39 |
|
show
|
2:06 |
So we built a really fine web application with dynamic templates and static files and all that awesome stuff that most web frameworks have, on top of FastAPI and that's great.<br>
It leaves us the option to do all the API side of things with the best API framework out there in addition to doing all the cool web things.<br>
But web apps are not fun for you, just you and yourself.<br>
The whole idea of a web app is you put it on the Internet with a domain and the whole world is now there to find your app through search, through other ways of getting the word out and hopefully start using it.<br>
So in this chapter, what we're gonna do is take the app that we built and deploy it.<br>
We're gonna put it out on a Linux server, on the Internet.<br>
We don't quite go so far as to map the domain name to it because we don't have a domain name.<br>
But it's really a, short, short step to go from what we're gonna do in this chapter to actually having a product on the Internet and it's gonna be awesome.<br>
Now, here we are in the github repository.<br>
Over here, you can see we have a link, cause we don't actually have source code for this chapter because we're not going to deploy the app we built here.<br>
In our other FastAPI course we already deployed an application, and it is literally identical to this application.<br>
Now, technically, the contents that show on the screen are not the same.<br>
I'll show you that, it's right here.<br>
We built this weather endpoint.<br>
So it's a, whether service or whether API but also you can see right here we've got things like static files, we've got this dynamic template and so on.<br>
It's actually exactly the same.<br>
If we click on it, it even does some cool API stuff right there.<br>
Apparently, it's lightly raining now.<br>
We're going to deploy this application and here's the source code with all the config files and stuff that you might want to work with.<br>
You can make a very slight adjustment to deploy the app that we're doing here.<br>
Given that we did such a polished job deploying that weather application on Ubuntu, out on the Internet, I think it makes most sense for us to go through and look at that example there.<br>
The adjustments are incredibly small, but hopefully you'll have a great time putting your web application out on the Internet for all the world to consume.
|
|
show
|
4:29 |
Now, before we jump into actually see deploying the weather application, I do wanna show you something that can be super, super important.<br>
A security issue, if you will, at least usability issue, potentially a security issue for your web application.<br>
So let's, let's go over here first and look at whether.talkpython.fm Remember, this is an API endpoint, and it does just arbitrary http exchanges to URLs that are not necessarily obvious.<br>
And so to help people consume this API, FastAPI automatically builds a really cool set of documentation.<br>
That's not obvious, so if you go to a FastAPI app and you type "/docs", check out what you get.<br>
Here's our one endpoint "/api/weather", and if we scroll down, there's all these different things that are exchanged.<br>
Like here's a forecast, and the forecast has wind, which is made up of this stuff.<br>
Now if we actually go and click on this, it'll let us explore it, to try it out, so we could put in like Portland.<br>
We could type in something there, and it shows you what the response is gonna be.<br>
Like look down here you have this.<br>
Oh, what you're gonna get back, it's gonna have a weather.<br>
the description and category, the wind, the units, the forecast and so on.<br>
These are the type of errors that you might expect, some kind of validation.<br>
Well this is cool, I mean it's really cool for this API, you know what?<br>
our web app has this too.<br>
Watch this.<br>
So we come over here and say "/docs" and look at that.<br>
It's every endpoint in our application, now many of these are public, and it's totally fine.<br>
But some of them might not be.<br>
What if we had a special, supposed to not be public, semi secret admin section?<br>
or we had other things that maybe we use, but we don't really want people to know about?<br>
You wanna show those here?<br>
Do you really want a form that lets people just arbitrarily post stuff over to the registration?<br>
Probably not.<br>
So there's two things that we can do to make this not show up for our website.<br>
It makes sense for an API but it doesn't make sense for a website.<br>
So if we come over here, the easiest and quickest thing to do.<br>
Remember, there's a ton of options when you create these FastAPI instance.<br>
One of them is the docs_url, we can say that's None and the redoc_url, we also say that's None.<br>
So this means just turn off the documentation entirely.<br>
I want this all to go away.<br>
So now if we go back to our site like this and we try "/docs", 404, there are no docs, can not do this, forget it, no docks for you.<br>
It could be that the reason you're using FastAPI is, most of this is a website, but there's some really cool APIs and you would still like that cool documentation for the APIs but not for the website.<br>
So we can do a slightly less intense thing over here.<br>
We can go down to our views and let's say we just want home and about to show up.<br>
We want those to be part of our documented API.<br>
Of course that doesn't make sense, but let's just say so.<br>
What we can do is we can go to these others where we have our router, our app, and we can say include_in_schema is False.<br>
That means exclude this endpoint from that docs URL.<br>
Let's do that for account as well.<br>
Now just put it on every single router.get, router.post.<br>
It's getting include_in_schema = False.<br>
Remember, our decision was to say, well let's have the packages and account stuff hidden, but let's actually have this part of our endpoint.<br>
Now, like I said, doesn't make sense.<br>
But if you had special API endpoints like we kind of touched on potentially over there, then this was how you would do it.<br>
Let's go back here and try our "/docs" again.<br>
And yes, we have docs, but only two.<br>
Only the two that we did not exclude.<br>
One for getting the "/" and one for getting home slash index, home slash about, the others are gone.<br>
But it's really important that if you don't want this HTML views to be listed and shown on your site, I cannot think why you would ever want that.<br>
You very unlikely, very unlikely you want that.<br>
You want to hide them, either the safest way to do it, is to just blast them out like this.<br>
Or if you do have APIs, you wanna be careful about that, then you can go through and just exclude everything you want to be hidden.<br>
That's it.<br>
Just make sure you address this before you put it on the Internet.<br>
Otherwise, you might find people poking around in places you didn't know that they would find
|
|
show
|
5:49 |
I often get asked: where should I host my web app?<br>
my Python web app?<br>
People either ask me that after taking one of my courses or they'll ask me how to do that because of the podcast or something along those lines, and I have one answer that I typically give.<br>
But I want to talk about the spectrum of the options here because not everyone fits into the same bucket.<br>
So at the, probably easiest to get started and certainly easiest to operate, we have these platform as a service type of hosting environments for Python web applications.<br>
So Heroku has been hosting many web applications as a platform as a service, and they have great Python support.<br>
So what does platform as a service means?<br>
It means you'll do something like point, you know, connect Heroku to a GitHub repository, if that repository has a requirements.txt or other Python indicators right at the top, it will automatically determine that it needs to run under Python.<br>
You add a file to say here's the execute command, and it'll just go on, create a server, set it up, make sure it runs.<br>
You push a new thing to that GitHub repo, you'll grab it, redeploy it with zero downtime, all of those kinds of things.<br>
So this is super helpful, but you don't have as much control here, right?<br>
You've got to fit into their ecosystem.<br>
You want to use a database?<br>
Great, you're probably using their hosted database, which is not that cheap, honestly, depends on where that money is coming from and what you're doing.<br>
But it's not nearly as cheap is doing it yourself, that's for sure.<br>
So Heroku is actually, if you're very unsure about working with things like running and maintaining Linux servers and virtual machines and stuff, Heroku or other platforms as a service are a really good idea to get started.<br>
My favorite place for hosting is Digital Ocean, and in fact, at the time of the recording, all of the infrastructure for Talk Python Training runs over at Digital Ocean.<br>
We've got about eight servers there, and we do all sorts of interesting things with them to make all of our APIs, our APPS and our web app and whatnot go.<br>
Digital Ocean just launched something like a platform as a service that Heroku has, called their app platform.<br>
So you might consider using that, Python is definitely supported there.<br>
What I use is just their, what they call droplets.<br>
This is their virtual machines you can go create.<br>
Notice, starting at $5 a month.<br>
So for literally $5 a month, we could get this Python FastAPI up and running, no problem.<br>
We have a lot of database requirements?<br>
Maybe we've gotta add a dedicated database server.<br>
If it's very, very lightweight, maybe we could actually put it on the same server.<br>
Anyway, it's quite cheap to get started.<br>
Linode is also really good, and they're comparable to Digital Ocean in that they have hosting of like these droplets, these virtual machines.<br>
They don't call them droplets, but same idea.<br>
They also both Digital Ocean and Linode have kubernetes clouds or clusters, if you want to run Docker.<br>
I'm not necessarily recommending that, but it's you know, if that's the way you wanna go, they both have great support for that.<br>
Also, notice up at the top here, this URL talkpython.fm/linode If you do want to go to Linode, use that and you'll get $20 off, get a $20 credit towards your account.<br>
Not gonna change the world and I, it just lets them know that I'm sending people over.<br>
I don't actually get paid, this is just part of their sponsorship of the podcast.<br>
But, you know, it'll give you guys a little bit off so go ahead and use that if you feel like it.<br>
Next up, we've got a couple of the big ones.<br>
We've got a AWS.<br>
Now, AWS is amazing, I use AWS services for various things, like generating transcripts, the first pass of transcripts for the courses.<br>
Delivering some of the video content and things like that.<br>
But I don't host my service there.<br>
I think AWS is massively complicated and massively expensive.<br>
To run that $5 VM we saw over at Digital Ocean or the similar one at Linode, how much does it cost?<br>
About %50 - $60 a month for exactly the same thing.<br>
Yeah, and the thing is, it's super, super complicated because AWS runs extremely large scale applications, things like Netflix and so on.<br>
So all the tooling is really dialed in for these advanced use cases, which means the simple case is not so simple.<br>
But there is a simple, simplified version of AWS, if you guys are over there, it's called Amazon Lightsail.<br>
And to run that $60 server over a normal AWS, you can get it for $3.50 on lightsail.<br>
Go figure, it's very weird the fact that they have these two things, but they do and my theory is that this is a direct response to Digital Ocean and Linode, and it's a very similar and simple way of creating things, so you might consider it.<br>
Again, I'm, I'm sticking with Digital Ocean.<br>
We also have Microsoft Azure, they let you run web apps as VMs, they let you run web apps as a platform as a service as well.<br>
So those are the hosting places that I would first go look at If I was hosting my stuff on the Internet.<br>
If I was was trying to take this FastAPI we built, put it somewhere that has good data centers, has good reputations, has good pricing, at least you know, excluding the bare VMs that say AWS and Azure.<br>
But the, one of the uniform things about this, all of them,is that they're gonna ultimately run on Linux.<br>
So final thing, we're really in this chapter gonna just focus on getting our FastAPI running on an Ubuntu virtual machine in the cloud.<br>
We're gonna actually create it on Digital Ocean, but that's just like a few clicks in some web app.<br>
And then, you know, you won't even know what server, what host you're on.<br>
You're just gonna log into the server remotely and do a bunch of steps.<br>
So really, what we're gonna do is figure out where we're gonna run our Ubuntu virtual machine and on the platforms as a service, even when you're not directly interacting with that server, you really are running on some kind of Linux machine the vast majority of the time.<br>
So understanding what's going on under the covers is probably a good idea anyway
|
|
show
|
3:51 |
Here we are inside my Digital Ocean account.<br>
Now over, under these projects, we've got Talk Python and then we've got this thing called Playground.<br>
If I had already clicked on Talk Python, you'd see we have a whole bunch of servers that are hosting many services and databases and all sorts of stuff going on over there.<br>
But this one is just a nice little empty space where I can work, so this should look roughly like what you would have.<br>
Now, here's the platform as a service, this Apps thing, but what we're gonna do is just create a droplet, and droplet is, you know, code word for I'm gonna create a virtual machine.<br>
So you can see you could create a server based on many different versions of the operating systems, we could do FreeBSD, Fedora, Debian and so on.<br>
And then within those you get to pick which version.<br>
I recommend, if you go with Ubuntu, you pick an LTS Long Term Support Version.<br>
Otherwise, you'll stop getting updates and that won't be fun.<br>
We could also go for containers.<br>
You can go to this place called the marketplace, and it will let you, like, grab a Word, pre configured WordPress server or whatever.<br>
But that's not what we're doing, we're gonna create a distribution, and then you pick, well, what kind of server do you want?<br>
I'm gonna pick the $5 server for this one.<br>
Now you might think, well, this is a toy server, it's not really gonna be able to do much.<br>
But these servers can actually handle a lot.<br>
And Python does not put much of a load on these servers.<br>
It depends on what you're doing, but this little wimpy server, unless you have got some kind of crazy computation stuff going on, if it's a relatively standard web application, it should be able to handle multiple millions of requests per month.<br>
So it's actually a pretty good starting point.<br>
The next thing we're gonna pick, we could add an extra hard drive.<br>
We don't care about that.<br>
You might need that, if you had say, like, tons of data in a database, you'd wanna put that over there.<br>
Could make sense.<br>
We're not gonna do that.<br>
Now, you want to pick a data center that makes the most sense for the consumers of your application.<br>
For many of us, that probably means either Europe or the East Coast of the United States.<br>
For us, our servers are in New York City.<br>
The reason is that's good for all of the United States and North America, it's also pretty good for Europe because it's a straight shot across the ocean, so that covers many of our users.<br>
We also have people all over the world, you know, places that are far from there, like Australia, New Zealand, which is not ideal.<br>
But we gotta pick one place and just, for us, East Coast of the US made a lot of sense.<br>
However, I'm on the West Coast.<br>
So just to keep things quick as local, well, we're gonna pick this, right?<br>
So you pick the one here that makes the most sense for you.<br>
Just be careful, if you're gonna create multiple servers like a web server and a database server and you want them to talk to each other, it's much, much better if they're in the same same data center.<br>
We could use VPC, virtual private networking, it's on by default, but we're not gonna do anything with it.<br>
Might as well turn on monitoring, this lets us look at the server through some of the management tools here.<br>
We're gonna use an ssh key which allows us to just log in, you can just go ahead and type ssh and go and register the ssh key.<br>
We won't have any username or password to mess with, gonna to turn them all on.<br>
Here's the project it's gonna go into on Playground.<br>
You could turn on backups, we're not doing that.<br>
So watch how quick this is.<br>
Oh, I should have given it a name.<br>
Well, be sure to give yours a better name, but I'm gonna leave this going in real time so you can see how long it takes.<br>
So I'm not going to cut anything out here.<br>
I'll just keep kind of rambling on.<br>
It should take usually about 30 seconds.<br>
So virtual machine is there.<br>
I think it's probably starting right now.<br>
Maybe some final startup scripts, for the first time are running and wait for it.<br>
That's it.<br>
I don't know, what was that, about 30 seconds?<br>
We can go back and check, but not very long.<br>
And so now we have a virtual machine.<br>
Over here, we just click that to copy the IP address.<br>
Ultimately, you wanna map a domain name over there.<br>
But basically we're done with Digital Ocean, that's it.<br>
That was all of the Digital Ocean that we care about.
|
|
show
|
2:04 |
I wanna say ssh root@ that address.<br>
Obviously, you'd probably put some kind of domain name there.<br>
But first time it'll say: you have never been here, are you sure you want to exchange your ssh keys with them?<br>
Yes, and just like that, we're in.<br>
So the very first time you see, like, this warning and whatnot and it says, you know, you really should do "apt update" and then "apt upgrade".<br>
This is, check for updates, apt update, and then apt upgrade is apply those updates.<br>
And if you've ever put anything on the Internet, you want to know, you know that you need to patch it right away like here.<br>
If we log out and then log back in, notice it has 18 security problems.<br>
That's not good.<br>
So let's go and just upgrade it right now.<br>
Oh, and I believe it's like checking for its own updates the first time it came to life.<br>
So let's see how long this'll take.<br>
Here we go, it was still doing like some background stuff with the upgrade system.<br>
Alright, so it should be up to date, or ready to update.<br>
When you already see these Linux headers, that means there's a new kernel upgrade as well.<br>
Okay, that took a moment, because there's an insane number of updates to apply, but no big deal.<br>
Now, if we log out and just log back in, you'll see that a system restart is required.<br>
Not always the case when you apply updates that that's true.<br>
But when do a kernel upgrade or major things, it is.<br>
So we'll just have to reboot.<br>
Wait about 5 to 10 seconds, and our machine should be completely up and running and ready for us to configure as our web server.<br>
Perfect, so here we have our web server up and running not a web server yet, it's gonna be made into one.<br>
But we have our Linux server running on the Internet safely because we've applied the patches.<br>
We still need to do things like set up firewall rules and other types of stuff to make it more safe.<br>
But it's a good thing to have here running, that we can start working with and configuring, and we'll do that throughout this chapter.
|
|
show
|
3:18 |
Now that we've got our virtual machine running on the Internet somewhere, in this case up on Digital Ocean, what we're gonna do is talk quickly about what applications and server, services on that server are going to be involved and how they fit together.<br>
So this little gray box represents Ubuntu, our server.<br>
What we're gonna first install, or first interact with, when we make a request to the server at least really we'll probably start from the inside out.<br>
But the first thing that someone coming to the server is gonna interact with is this web server called Nginx.<br>
Now Nginx serves HTML and CSS and does the SSL and all those cool things, but it's not actually where our Python code runs.<br>
We don't do anything to do with Python there.<br>
We just say you talk to all the web browsers, all, to the applications, everything that's trying to get to the web infrastructure.<br>
This thing is where they believe they're talking to, and it is what they're talking to.<br>
But it's not where, what is happening.<br>
That's not where the action is, right?<br>
Where the action is, is gonna be in this thing called gunicorn You saw that we used uvicorn, which is the asynchronous loop version of gunicorn to run our FastAPI, but gunicorn is more proper server that is going to do things like manage the lifecycle of the apps running.<br>
So, for example, if one of the apps gets stuck and that process freezes up, gunicorn has a way to run in supervisor mode.<br>
So it can say, actually that thing is stuck or it ran out of memory, let's restart it so the server doesn't permanently go down, it's just gonna have a little glitch for one user, and then it'll carry on.<br>
In order to do that, gunicorn is gonna spin up not one but many copies of our FastAPI application over in uvicorn, which we've already worked with.<br>
And this is where our Python code that we write, our FastAPI lives.<br>
So when you think of, where does my code run?<br>
what is my web app doing?<br>
It's gonna be this uvicorn process, and in fact, not one but many.<br>
For example, over Talk Python Training, I believe we have eight of these in parallel on one of our servers.<br>
So when a request comes in, it's gonna hit nginx, it's gonna do its SSL exchange and all those things that the web browsers do with web servers, nginx is going to realize, oh, this request is actually coming to our FastAPI application.<br>
Depending on how we've configured it, it's gonna send a request either over HTTP or Linux sockets directly.<br>
gunicorn says okay, well, we've got this request for our application, and there's probably a bunch going in parallel.<br>
Which one of these worker processes is not busy and can handle requests?<br>
Well, this one.<br>
Next time a request comes in, maybe it's this one.<br>
Another request comes in, maybe those two are busy and it decides to pick this one.<br>
So it's gonna fan out the requests between these worker process processes based on whether or not they're busy and all sorts of stuff.<br>
So it's gonna try to even out the load across them, especially it'll know if they're busy and not overwhelm any one of them.<br>
So this is what's going to be happening in our server and we're gonna go in reverse.<br>
We're gonna install uvicorn and our Python web app, then we're gonna set up gunicorn to run it.<br>
And once we get that tested and working inside the app, inside the server on Ubuntu, then we're gonna set up nginx and open it out to the Internet and make this whole process that you see here flow through.
|
|
show
|
1:10 |
Before we start configuring our server, I wanna set up Oh My Zsh, or Oh My Z shell Now, this is absolutely not required.<br>
But I find the ability to go back through history and use frequently commands and adapt them and stuff and just knowing how to work with git, you can see, like in the little screenshot here, the prompt changes based on where you are in a git repository and things like that, just a lot, a lot nicer.<br>
So I'm gonna go over here and I'm gonna copy this bit right there because that's how we're going to install it.<br>
And let's to reconnect to the server.<br>
I renamed the host name because it was driving me crazy.<br>
So now it's weatherserver.<br>
And in order to install this, I have to say apt install zsh because this is based on Z shell.<br>
And then run this, wait for a minute, make it the default.<br>
And now we have a little bit of a nicer prompt.<br>
The next time we log in, actually gotta log out twice the first time because you log out of z shell, then log out of bash.<br>
We'll have this, you see, our little new shell is here.<br>
And we'll see maybe some stuff as we go through it.<br>
But this is just a nice little step.<br>
I find this a much, much nicer way to work with servers.<br>
So I encourage you to set this up.<br>
Don't have to, but I find it makes life a little bit nicer.
|
|
show
|
3:37 |
I've copied what we built over in chapter seven into chapter eight and made a few minor changes.<br>
And I did this without recording it, because you'll see they're just a bunch of config files we're gonna have to set.<br>
Like, for example, here's our nginx config file.<br>
We never type that from scratch, we find some example, and we adapt it.<br>
So that's what I did, we'll talk about that in a minute.<br>
But we also, I also put in here, there's a script that sort of takes us through these steps to set up our servers So we've done our upgrade and patch.<br>
We've installed Z shell.<br>
Now we're gonna need a few other things in order to further secure our server and to make it ready, to get it ready to run Python.<br>
For example, make sure we have the Python3-dev tools so we can install things and so on.<br>
So let's go over here and put the build-essential, git, zip, and some other things.<br>
Not all of them are required, but they're all useful.<br>
Now we have things like git set up.<br>
That's cool.<br>
Let's set up Python, on Linux when we install Python3, it doesn't necessarily come with pip or with virtual environments.<br>
So we're gonna install all three of those now, and just talking about z shell.<br>
If I type apt for stuff that I could have done, because I typed sudo, didn't I?<br>
So if I type sudo, you can just see it'll only cycle through the sudo stuff as you arrow whereas bash, it just goes through the history and things like that.<br>
So there's a bunch of little nice touches.<br>
All right, so now we should be able to run Python -V, Python3 -V.<br>
There we go, 3.8.5.<br>
Now we're gonna do a couple of things here to make the system a little more secure.<br>
We're gonna do three things in particular.<br>
We're going to set up what's called fail2ban and to do fail2ban, what this is if somebody tries to log in over ssh and they fail either through user name password, which we don't have set up or through ssh keys, if they do that too many times, then they're going to be banned from attempting to log in.<br>
So this is a nice little service to avoid sort of dictionary attacks or brute force attacks against logging in.<br>
We also want to turn on a firewall.<br>
Linux, Ubuntu comes with a firewall, uncomplicated firewall, uwf.<br>
And what we wanna do is we wanna say, allow ssh traffic and allow web traffic.<br>
So port 80 to start things off on HTTP and 443 to allow SSL HTTPS traffic.<br>
Other than that, we won't allow nothing.<br>
And when we turn it on, it says, if you have not allowed ssh and you say turn on the firewall, you're never coming back.<br>
But luckily, we have.<br>
So let's close this and just reconnect to make sure it's fine, it is.<br>
An then the other thing is, what is our user?<br>
I'm root.<br>
Do you think running as root is a good idea?<br>
No, not a good idea at all.<br>
So we're gonna install, create a new user.<br>
A user that doesn't have log in permissions, apiuser and we're going to run our web application that, that way, in case somebody happens to break through and take over our system, they're only gonna be able to do what a apiuser can do, not what root can do, so that's good.<br>
We also want to create some log files, locations here and then give that user permissions I think I probably, they must have to exist first.<br>
So let's do that.<br>
And then we can say give them modify access to where the web app needs to keep its logs.<br>
All right, so we're not, we don't have our code on here yet.<br>
We don't have the libraries needed to run set up, like Uvicorn or FastAPI But our server is much closer.<br>
We've got fail2ban, we've got the firewall running for only the ports we want to explicitly expose, the three.<br>
And then we've got our user that is a less privileged user to run our web app as.
|
|
show
|
4:16 |
Next up, we need to get our source code on the server.<br>
Well, guess where the source code is?<br>
It's right on GitHub.<br>
In fact, it's right here on this public GitHub repository.<br>
So what we're gonna do is just clone it.<br>
And this is a really good way to get our code over there.<br>
You make a change, go into the server, do a "git pull", restart the server.<br>
In fact, over at Talk Python Training, we have a certain branch in our GitHub repository called production.<br>
And if we push to that branch, we've got some infrastructure that will automatically look at that, get the new version, install the new requirements, restart, you know, take the, one of the servers out of, a load balancing situation, reconfigure it, start it up, then put it back in.<br>
Really, really, sweet.<br>
So all we've gotta do is push to GitHub and that deploys to our Linux server.<br>
We're not gonna go that far because that's fairly involved.<br>
So what we're gonna do is just manually do git pulls if we make any changes But that's a really nice way for us to get the source code on the server and keep it up to date.<br>
So we're gonna do these two things.<br>
Now, I'm gonna clone into app_repo so we don't have some huge long name.<br>
There we go.<br>
And if we see over app_repo, here's our source code and chapter eight, this is where we're working.<br>
Okay, so we've got our source code over here, and you can see we've got all of our files.<br>
Now, one thing I do wanna do, we're gonna need to run pip install requirements to set up our requirements.<br>
However, I realized I did skip a step.<br>
What we want to do is we actually wanna create a virtual environment for our web application, okay?<br>
why do we want to do that?<br>
Well, when we pip install requirements here, we're changing the server set up.<br>
And just like locally, we probably don't really wanna change the server set up.<br>
Also, having a virtual environment means if something goes dramatically wrong with your Python set up, you can just erase that virtual environment and recreate it.<br>
It's not like something that you've got to reconfigure the server before, so that's a really big advantage.<br>
So what we're gonna do is we're gonna create this virtual environment and go back to our chapter eight, here we go.<br>
Now we have our virtual environment we can pip install -r requirements.txt And with that done, we should almost, almost be able to run our app.<br>
So we should be able to say Python3 main.py and that'll run it, right?<br>
But there's a problem, it's a secret, do you remember?<br>
Our settings.json is not there.<br>
Remember we have our settings template like that, and it says you're gonna need to create a settings.json file with your actual keys.<br>
So I'll just nano settings.json.<br>
Actually, let's start from our template.<br>
And then all we gotta do is, we don't need this.<br>
It just says, put your key here.<br>
So I'm going to, go and put my key right there and save it.<br>
Off course I don't wanna share my key with you, this is where your key goes.<br>
So I'm gonna pause the recording and then save this, should be able to run.<br>
You do that for your key.<br>
So with our virtual environment activated, the requirements installed and the settings.json there, we should now be able to come here and actually run our main.<br>
Yes, look at that.<br>
How awesome.<br>
So uvicorn is running on our server as we hoped.<br>
Now, how do I test it?<br>
If I test it, I can't test it here because this window is, you know, it's busy, busy running the server.<br>
But let's go open up another window here, another terminal, and we can curl that url.<br>
Now curl is fine, but there's a really cool library.<br>
HTTPie, that is really much nicer for doing this kind of stuff, but look at that.<br>
What do we got?<br>
Here's our Copyright Talk Python Training.<br>
Here's our weather service, our RESTful weather service, right there.<br>
So it looks like our service, our server's running.<br>
You could even see it process the GET over there.<br>
So that is fantastic.<br>
We're very, very close.<br>
Now we need to find a way to make the system run this, cause you can see when I hit Ctrl + C or if I would have logged out of my terminal, obviously the server stops.<br>
That's not good.<br>
We just want this to start when that server turns on.<br>
When you reboot, it just comes back.<br>
It just lives as part of the server.<br>
So we need to create a systemd service or daemon over there so that it will run our Python web app.<br>
But it's basically working, isn't it?
|
|
show
|
1:12 |
On the server, it's only purpose is to run this web application.<br>
To run this web application, we need to have the virtual environment activated.<br>
So what I'm gonna show you is that.<br>
Lets ask really which Python3 real quick.<br>
So we know where it is, there.<br>
Having that directory is gonna help.<br>
What I'm gonna propose is that we change our login.<br>
So as we log in, it just activates that virtual environment.<br>
Otherwise, we do Python stuff, pip and Python and so on.<br>
It's gonna work with the system one.<br>
We almost never, ever wanna change it.<br>
We almost always just want to work with this one virtual environment, so let's just make that the default.<br>
So we'd say nano ~/.zshrc, go down to the bottom here and we'll say source that bin, instead of Python, we'll say activate like that.<br>
If I exit out and I log back in.<br>
Look at this.<br>
Yes, that's the right one.<br>
If I "pip list", it's all the stuff that we just installed, FastAPI and so on.<br>
That's super cool, right?<br>
So I totally recommend that you set it up so when you log into your server, it just sets up the environment for that one on web app.<br>
If you've got 20 we apps and all sorts of things going on, maybe it doesn't make sense.<br>
In this simple world, I think this is the best thing to do.
|
|
show
|
4:13 |
Now we want our web app to work all the time on this server.<br>
And if we try to request it, mmm not running, why?<br>
Because I logged out, that's silly.<br>
We just want it to be part of the server.<br>
And so what we're gonna use is something called a unit file over here, to set up a systemd server.<br>
So this is going to tell the system: run this command when you boot, basically.<br>
So it says, go to this directory, run this command.<br>
And let's, just let me do some line breaks here to make this mean stuff.<br>
So it says we're gonna run gunicorn, we're gonna bind to that address.<br>
We're gonna run four copies of you uvicorn as workers.<br>
I talked about having multiple worker processes where our Python code and our FastAPI code is actually gonna run.<br>
So if you want 10, you put 10 there, 4 is fine though.<br>
And we're gonna run not WSGI gunicorn stuff, but ASGI uvicorn ones.<br>
We're gonna go to the main module and pull out the app api instance that's configured.<br>
Tell the process it's called that, make sure that that's the working directory cause sometimes this is little wonky.<br>
Set a log file there for access, set the error log file there, then run as the user apiuser.<br>
It's a lot, right?<br>
But that has to be all one line because it, the way it's issued.<br>
So I'm gonna just copy that.<br>
Now, you wanna make sure this is going to work before you try to set it up as a system daemon.<br>
Okay, so just making sure that line is gonna do something is a good idea, and it doesn't.<br>
Why?<br>
cause there's no gunicorn.<br>
So we've got to go back over here and we've got to install gunicorn, and gunicorn and uvicorn on the server require these two libraries as well.<br>
So we're just gonna do those three things.<br>
Ah, looks like a pip install wheel would have made that a little bit nicer, but I'll change the script for you all, here we go.<br>
Anyway, all those installed slightly, slightly quicker.<br>
Now with those set up, let's go and try to run our command here.<br>
Run uvicorn this, what happened?<br>
Nothing.<br>
And that's good, all the output is going to these log files.<br>
Okay, so let's just make sure that we can connect over here and now we'll be able to http localhost:8000.<br>
Nice.<br>
See, for example, color coding, you see the header information, all kinds of good stuff, it's better than curl, but still working.<br>
Good.<br>
So let's get that to the side for a minute and we get out of here.<br>
It looks like it works.<br>
One thing that may be helpful sometimes, specially if you're getting errors is if you just try it without the logging.<br>
That way you see all the messages right here, and if we go back and do this again, ah you still don't see that, but at least you see the start up process.<br>
If something goes wrong, often, you'll see the error right there.<br>
Ctrl+C to get out.<br>
So it looks like that command is working, that means it's likely going to work if we set it up as a system process instead of just typing it in.<br>
So what do we gotta do to do that?<br>
We just copy that file over here, like this.<br>
And then we say we want to use the system control to try to start the thing called weather.<br>
Right now, if we look, not there.<br>
If we try this, so this has kicked off the thing as a server in the background, now it's just running.<br>
Yes, it works.<br>
How cool is that?<br>
And you can ask, go up here, you can ask "status", what's it doing?<br>
Look, there's four worker processes all running over here, that's great.<br>
Now we also want to say: do that when I reboot, not just this one time.<br>
It's the way we do that is, we say enable, perfect.<br>
And now we could do a test, reboot and just make sure when we get back, things are still hanging together.<br>
So give that about 10 seconds, you don't have to do this, it would've been fine, but just, you know, peace of mind to make sure that it really is set.<br>
And it's back.<br>
Let's do our http localhost Yes, all right.<br>
We re booted the server and now our service is running still, so perfect.<br>
We've now set up gunicorn and uvicorn to always run our app.<br>
But you still can't get to it from the outside.<br>
remember it's listening on localhost but this is a huge start.<br>
Actually, this is the hardest bit of it all.<br>
If we get this working, we're pretty golden.
|
|
show
|
3:29 |
The last thing we need to do really, is to set up in nginx so you can talk to our web api from the outside.<br>
So this is super easy, we're gonna install nginx, I'll go back here and off it goes, it's going to stall a bunch of stuff, nginx is a fantastic front end web server.<br>
Do all sorts of cool things with it.<br>
Okay, now we're gonna need to, just like before, copy a configuration file over cause that's how this all works.<br>
And in order for this to work, what we want to do is we're gonna have a domain name, imagine like weatherapi.talkPython.com or something, but because this is not a real thing with a real domain, it's just a simple demo, I'm gonna just put the IP address here as well, let me actually get the IP address, copy it here.<br>
You wouldn't normally do this, probably, but because we don't have a domain name and I don't want to mess with DNS and wait for it to propagate, we're gonna do that.<br>
So the thing basically says I'm gonna set up a server to listen on port 80 to the domain and maybe IP addresses.<br>
There's a set of static files that live here.<br>
So if someone asks for /static, give them files out of that folder, Otherwise, anything else go and proxy_pass over to port 80 on localhost.<br>
That means talk to gunicorn, which will fan it out to the uvicorn workers, and that's about it.<br>
There's really not a whole lot else going on, but notice this change, we've gotta get this up to the server.<br>
Let's go commit that and then push that to GitHub and then we'll go here and we'll cd to our apps/app/repo We'll git pull, you can see our server change there, and then we're pretty much set.<br>
We just gotta copy this file, which you might as well copy paste, so you don't get it wrong and then we need to tell it also, we want to remove the default, just nginx's installed file and make it use ours and we're going to tell it to start when the server starts and then we're gonna restart it so it re reads the config files.<br>
All those things are good, now if we try http, remember that localhost:80, port 8000.<br>
What if we just do local host?<br>
This will talk to nginx.<br>
Oh oh oh, it's working.<br>
So go over here, I need my IP address back.<br>
Let's go on the internet and see if this works, fingers crossed.<br>
Yes, look at that.<br>
Our server is up and running, and all we gotta do is go to the DNS and put a proper domain name there, and we would have our thing fully on the website.<br>
Up and running on our Ubuntu to server.<br>
So what does it mean to have a domain name?<br>
You can just go to wherever your DNS is and tell it that this IP address is where that domain name goes and make sure that that domain name is right here.<br>
You can have multiple domain names, like you can have, all right, we could have reports.talkpython.fm, whetherapi.talkpython.fm and so on and so on.<br>
Whatever ones you wanna point to the server, as long as there in there, the rest will just flow through the system.<br>
So super cool, we got this working.<br>
Let's just check that our API does its thing.<br>
Yeah, we went out to OpenWeatherMap, we've got it, put it here.<br>
Now, of course, it's using this caching.<br>
Let's actually do an inspect element, look at the network, make a call again, look at that, 45 milliseconds, 39 milliseconds.<br>
That's all the way to the server across the Internet, down to San Francisco or wherever it was and we picked it.<br>
Very, very cool, so our job here is basically done.
|
|
show
|
2:05 |
Alright.<br>
Final, final thing.<br>
Notice this up here says the connection is not secure.<br>
That's weird.<br>
So browsers these days and Google and so on are making sure that we have HTTPS for almost everything.<br>
And if they're not, they're not secure.<br>
Even if this was a domain name, would not be happy.<br>
So how do we do that?<br>
Do we go and buy an SSL certificate?<br>
That's so 2 years ago, now we wouldn't do that.<br>
So what we would do is we would go over here and we would add this repository like this over here.<br>
And once we've done that, we're going to install Python-certbot-nginx.<br>
So this is gonna use Let's Encrypt.<br>
Python3 yeah, okay, good, beautiful.<br>
And I'll update that as well here for you.<br>
And then we would just run certbot --nginx like that for this domain.<br>
And it's gonna look and says, great, what's your email address?<br>
I'm michael@talkPython7.com I'm gonna put a little thing here so it doesn't actually email me or contact me, because this is not really gonna work.<br>
And it says: do you accept the terms?<br>
We do accept the terms, I don't want to share that cause it's fake, so it's gonna try to go get a certificate, but it's gonna fail.<br>
Why did it fail?<br>
Cause it said, alright, great, you want to set up a certificate for weatherapi.talkPython.com, is the place I'm running on where that URL, over that, resolves to.<br>
Like, am I actually on the weatherapi.talkPython.com server?<br>
No, because that domain doesn't go anywhere.<br>
But if I had pointed it here and waited for the DNS to resolve, this would just automatically install SSL on server and it would just keep it up to date.<br>
Beautiful, so that's all we gotta do to make SSL work.<br>
But of course, it requires this DNS thing so you can't create fake servers elsewhere and so on.<br>
Not gonna actually go through this step by doing the DNS, but it's super simple to follow.<br>
There's a few questions about whether or not you want to redirect if somebody hits the non SSL one, say yes.<br>
So this is a really beautiful, free an automatic way to keep an up to date SSL certificate on your web server
|
|
|
16:21 |
|
show
|
1:13 |
Well, we've come to the end of the course and congratulations on making it this far Hopefully, you've worked through building the application along with me as we've worked through all the different chapters and the different technologies.<br>
So it's time to bring it home.<br>
You're now ready to build production grade apps, not just APIs but full on web applications with FastAPI.<br>
And let me just reiterate, the reason I think that's super valuable here.<br>
Sure, there are many web frameworks, and FastAPI is a great API framework, but you often want to have both.<br>
There's often some web UI component to your app and an API.<br>
You don't wanna have those things be separate.<br>
You wanna have the ability to use the same framework.<br>
So if we build our web applications with FastAPI instead of like, say, the foundation like Starlette or a completely different framework like Django, we have this ability to seamlessly move between them.<br>
And I think that is super, super, powerful.<br>
FastAPI is such a great framework that it actually makes one of the best web application, the dynamic HTML side of the web, really, really nice as well.<br>
So I think you're gonna get a lot out of this.<br>
And during this chapter, we're going to go through all the different topics that we've covered during this course and just do a quick review of what you've learned.
|
|
show
|
2:23 |
The first major thing that we covered was, how do we get started with FastAPI?<br>
Let's build the absolute most minimalistic web application, not just API endpoint, but web application with FastAPI.<br>
And the fact that it fits in that square grey box that we're about to light up, that tells you, it's not too complicated, is it?<br>
So we're gonna need two things to get started.<br>
We've gotta import fastapi so we can use it.<br>
And then, in order to run the application created with FastAPI, we need a web server.<br>
So we're gonna use uvicorn, which is a fantastic, production grade, ASGI, Asynchronous Web Service Gateway Interface Server.<br>
We start by creating an app instance from the FastAPI, our FastAPI instance.<br>
I'm calling it app here, sometimes it's called api, sometimes it's called app.<br>
Doesn't really matter as long as you're consistent.<br>
Then we define a function that's gonna be called for a given URL when we request it with a web browser.<br>
So we say app.get("/").<br>
That means if we do a basic normal HTTP request against the, just the server.<br>
This is what's going to run, not a POST or other HTTP verbs, just a GET standard web request.<br>
And then, in here, we're going to generate a message very, very simple in this case, we just have a header that says "Hello web!".<br>
Now remember, if we return just the string by itself, return "<h1>..." and so on, FastAPI is an API framework first, with this ability to do HTML second.<br>
So if we did it that way, we would get a JSONResponse, which has a string "Hello, web!".<br>
That's not at all what we want.<br>
We wanna have a web page, not an API response, right?<br>
So we're gonna use one of the specialized responses, it really comes from Starlette, but we'll find that under fastapi.responses.htmlResponse.<br>
Doing it this way, we tell the web browser what you're getting back is a web page, not some JSON data exchange.<br>
This is gonna be our home page here.<br>
Not very impressive as you saw it when we got started, but we built on it throughout the course, and then finally, in order to run our application, unlike Flask, you can't say app.run().<br>
Instead, you say uvicorn.run(), and then, if you'd like, you can specify the host and the port.<br>
The port defaults to 8000.<br>
The host, I believe, defaults to 0.0.0.0 or listen on the Internet.<br>
Not a big fan of that, so I'll just listen on localhost here.
|
|
show
|
1:59 |
While returning the string "<h1>Hello web!</h1>" from our API method, our view endpoint there was fun, it is not at all the way we should be building web applications.<br>
What we should be doing is getting a standalone, dedicated HTML template language to generate dynamic HTML from the data our web app provides it, and then turning that into proper HTML with all the cool stuff, like global shared layout pages and little smaller, customized, specialized pages that just fill in the holes about what's unique for each page We also looked at three possible candidates to solve this problem.<br>
Jinja2, Mako, and Chameleon.<br>
Now Jinja2 is sort of built-in, sort of built-in to FastAPI.<br>
It's not entirely built-in cause you've got to install Jinja2 in order to use it, but there are some capabilities in there.<br>
Chameleon, not at all, but we went through the different languages and we said, well, what are the advantages and disadvantages of each?<br>
And I said, I'm going to use Chameleon because I believe, at least for the things that I value most, it is the best template language.<br>
Remember, you don't get to write arbitrary Python, which some people see as a disadvantage.<br>
But I see that as a way to enforce you to write better factored code and to put that code not in your template, but somewhere better where it belongs.<br>
Also, the templates are proper HTML, like this thing you see here.<br>
This is valid HTML with, with some weird attributes that .gitignored.<br>
But that's OK for HTML, as opposed to Jinja, which has all these begin end blocks and little funky segments that are not valid HTML.<br>
So for that reason, I said, hey, I think this is the one we're going to use.<br>
We used it, we used my fastapi-chameleon decorator in order to make this incredibly easy to use.<br>
If you don't like Chameleon, again, there's the fastapi-jinja template that is almost exactly the same thing, but for Jinja2, so you can just use that one as well.<br>
Those are really the two best choices, I think.<br>
I definitely prefer Chameleon so here we go and you can choose whichever one you like.
|
|
show
|
2:17 |
Our next major area that we focused on were two design patterns, view models for the most part, and we also touched on this one that I was calling services, not web services or APIs, just services to the general larger application.<br>
And I think the view model pattern is worth focusing on a little bit here in the review.<br>
Remember, with the view model pattern, it plays a lot of the same roles that maybe Pydantic models do for the API endpoint.<br>
Because Pydantic and FastAPI go so well together, it makes sense that maybe we should just be using Pydantic models for this type of exchange here but the problem is that Pydantic models, while fantastic, they completely just give up if they run into a problem.<br>
And we saw, one of the really important things was, I'd like to be able to show a form, let people type into that form, try to submit it, if that doesn't work, I need to reload the form with the old data.<br>
Well, that's not how the Pydantic models work.<br>
They just say you gave me something wrong, go away.<br>
And so for that reason, we have, a more forgiving pattern that is also quite similar, but a little more manual called view models.<br>
The idea is, there's a known set of data and data types and rules for validation for an HTML template, the white thing here on the left.<br>
And so let's create a Python class that knows about all that required data, how to get it from the page, how to convert it, how to exchange it, what this validation rules are and completely separate that from the rest of the behavior.<br>
So, for example, if our action method here was to register, create a new user.<br>
We shouldn't have to worry about all that validation.<br>
We should just be able to go: is it valid?<br>
No, tell them to try again.<br>
Is it valid?<br>
Yes, then create the user with the data that we got, send them a welcome email, put them in the database, all the things that you would expect to do, the actual business of this endpoint this view or action method rather than all that validation and data exchange and conversion.<br>
So that's the idea of these view models, I think they're super, super valuable in web applications.<br>
They make things like testing, the validation, all by itself without getting into web testing and fake requests and all that kind of stuff really, really easy.<br>
So this view model, I think it's a great pattern.<br>
Take it or leave it.<br>
But I suggest that you try it in your apps, I think it's highly valuable.
|
|
show
|
1:55 |
The next thing that we looked at was, how do we get users to register or log in to the website?<br>
We talked about a lot of interesting things like, how should we structure our User class?<br>
And what kind of validation should we have?<br>
And how do we store it with things like passlib and so on?<br>
But one of the important takeaways, the more broad lesson from that chapter really was about: How do I create an HTML form that I let the user type into, with validation that they can then submit back to the site?<br>
The starting point for that was understanding we're probably best off using this GET > POST > Redirect pattern, and it's incredibly easy to do in FastAPI.<br>
So we're gonna set up a page that we GET, that's the empty form, we're gonna fill it out like, hey, click here to register for the site and the empty form fills up, shows up.<br>
We fill it out by editing it locally, and then we want to save it.<br>
Now, assuming that we passed all the local validation like the HTML5 required and is an email and so on, we're gonna submit that through an HTTP POST, that's how forms are submitted to a different method in our website.<br>
we put the @app.get() on the first one, we put @app.post() on the second one, even though they have the same URL.<br>
Over there we used our view model to validate and convert our data, if everything worked, we saved that back to the database, and then we issue a 302 Redirect.<br>
Remember, with FastAPI, a regular redirect keeps the POST behavior, the POST verb flowing.<br>
So we wanted to issue a 302, it's over here, go do a GET request to that, which was the more standard web behavior that you would expect.<br>
If this didn't work, we just have the view model return the data that they had entered back to the form by returning the dictionary with the same template.<br>
Easy as it can be.<br>
This is the way almost every HTML form, that's not some kind of JavaScript single page app driven type of thing, this is how it works and how it's easiest to implement in FastAPI.
|
|
show
|
1:59 |
The next major area we focused on was to actually start saving all of this data to the database.<br>
Remember, we had taken the top 100 packages, and once we were able to save stuff to the database, we imported those there.<br>
We started having our users actually saved in the database when they registered and then validated from the database when they logged in and so on.<br>
So a couple of things that we did there for those models was we created this declarative_base.<br>
So sqlalchemy, extensions, declarative, declarative_base.<br>
And this is really just a class, as if you type the word class space something, but it's more dynamically generated.<br>
And the idea is: Everything that derives from this class will be a table that is managed by SQLAlchemy.<br>
You have Package, Release and User because they all three derived from this particular instance of this base class, this particular type we created here.<br>
It means that when we go to that base class and say, create all your tables or you have relationships or things like that, all those tables are gonna be related and created through that one action.<br>
And it's all this base class, the SqlAlchemyBase class, that makes that happen.<br>
And then, for each one of these classes, in this case Package, we're going to do a couple of things.<br>
We define a dunder table name and then we're gonna give it some columns that are also fields in our classes, so an id, in this case we're gonna say this is an integer, it's autoincrementing, and it's a primary key, so we don't have to set it, just as we insert stuff in the database, it will automatically get this primary key set appropriately and uniquely from the database.<br>
Then we'll have a summary, which is a string, a size which is an integer, a home_page, which is a string.<br>
We even saw we can set up really cool relationships like for example, this package has a bunch of releases.<br>
So instead of doing additional database queries manually, we're just gonna set up the relationship.<br>
And if we interact with the releases collection, it's automatically going to get that from the database, either because we've done a join ahead of time knowing that's gonna happen or through a lazy loading thing, and it'll go back to the database and get that data if we need it.
|
|
show
|
2:10 |
We saw that to truly unlock FastAPI's massive, massive scalability and performance, we have to make our view methods, our endpoints async because when you have a web application, most of the time, what is it doing?<br>
Its waiting on external things.<br>
It's waiting on a queue, it's waiting on a database, it's waiting on some microservice or external third party service.<br>
All of that waiting can be completely put aside and allow your app to do other work, maybe start other things that then in turn, run awaits on those, right?<br>
So most of a request is actually made up of waiting.<br>
And if we have a synchronous web server like Flask, like much of Django, like Pyramid and so on, the traditional WSGI applications, they don't have this capability.<br>
So when a request comes in, well, the first one gets processed right away, and it takes as long as the expected time, the green one.<br>
But because the server's busy, until we get done with Response 1 we can't start with 2, it actually takes quite a bit longer and poor old fast little Response 3, it's queued up, put behind in the line here for those two requests So even though it's short, it actually takes the longest of all three of these.<br>
We dive in, we see the problem really is that we're mostly just waiting on other stuff, we're like 80% weighting, 20% doing something computational on our end.<br>
If we just rewrite this to say await database query, await web service call and make our view methods async, FastAPI will completely rework how it runs our code, and it will process Request 2 while it's waiting on the database for 1.<br>
And then it will be waiting on the database for 1 and 2 so it can zip over, get Request 3 done really quick and then get back to waiting for the others.<br>
And it really, really amplifies the scalability.<br>
Now the scalability, of course, depends on the scalability of the thing on the other end, right?<br>
If you have a database and the actual roadblock, was the database is really wimpy or it's poorly written without indexes or something like that, eventually you're gonna hit a limit where just further down the line, you're waiting on something else, and it won't make it that much better.<br>
But in a lot of cases where you have a fast database or external APIs or many different services, this is gonna see a huge improvement for your app.
|
|
show
|
2:00 |
After we were happy with our app and we got it built and running, it was time to put it on the Internet, and we decided Nginx plus Gunicorn would be a great way to deploy our FastAPI web application.<br>
Nginx is by far one of the most popular front end web servers that handles things like static files, it handles SSL, and you know, upgrades requests to things like HTTP2, all sorts of cool stuff that's happening over there.<br>
It will be the front line server that our clients actually talk to.<br>
But when it comes to running, our Python code, our FastAPI code, we're gonna do that, managed by Gunicorn.<br>
We already saw that Uvicorn, which comes from Gunicorn, is how we were, we've been running our app so far, and we're gonna continue to use that in production, but not just by running it, but by having a bunch of them that Gunicorn can manage.<br>
So it's gonna spin off 5, 10, who knows how many makes the most sense for your server and the number of requests you're getting, but multiple numbers of your process as if you had started it just on the command line multiple times.<br>
And then when a request comes in, Nginx is gonna figure out where it goes.<br>
If it's a Python request, it hits Gunicorn and says, hey, drive one of these Uvicorn worker processes that's not busy.<br>
Well, this one, it's not busy, it can handle a request.<br>
Another request comes in, says here, this one's not busy this time.<br>
Now then maybe those two are busy, another request comes in, it picks this other one this next time.<br>
And this is generally how our web application works.<br>
We saw that setting up Gunicorn to run, we had a special config file to do that as a systemd service.<br>
And then we have a special config file to set up Nginx, and then we also had a whole script to actually build out the Ubuntu server just the way we wanted it.<br>
It might be a lot to take in at the beginning, but if you go through it a few times, you'll figure out what the important parts are to pay attention to, and the rest, you'll just copy and paste; and remember, this script goes there and then we're good to go.<br>
Not too bad.<br>
So good luck running your app in production.
|
|
show
|
0:25 |
Well, that's it.<br>
You've come to the end of the course.<br>
Thank you.<br>
Thank you.<br>
Thank you for taking this course, for spending this time together.<br>
I hope you really learned a lot.<br>
I hope you enjoyed it.<br>
And most importantly, I hope you built something awesome.<br>
If you did, and you get it out on the Internet, be sure to shoot me a message.<br>
I'm @mkennedy over on Twitter, or you can find me through the Talk Python sites in a couple of ways.<br>
So thank you for taking this course and best of luck on your project.
|
|
|
54:50 |
|
show
|
4:07 |
One of the absolute pillars of web applications are their data access and database systems.<br>
So we're going to talk about something called SQLAlchemy and in many, many relational based web applications this is your programming layer to talk to your database.<br>
SQLAlchemy allows you to simply change the connection string and it will adapt itself into entirely different databases.<br>
When you use a local file and SQLite for development maybe MySQL for testing and Postgres for production I'm not really sure why you would mix those last two but if you wanted to, you could with SQLAlchemy and not change your code at all just simply change the connection string.<br>
So SQLAlchemy is one of the most well known most popular and most powerful data access layers in Python.<br>
SQLAlchemy, of course is open source you'll find it over at sqlalchemy.org.<br>
It was created by Mike Bayer and his site is really good.<br>
It has tutorials and walkthroughs for the various which you can work with SQLAlchemy one for the object relational mapper one for more direct data access, things like that.<br>
So why might you want to use SQLAlchemy?<br>
Well, there's a bunch of reasons.<br>
First of all, it does provide an ORM or Object Relational Mapper but it's not required.<br>
Sometimes you want to programming classes and monitor your data that way but other times you want to just do more set based operations in direct SQL.<br>
So SQLAlchemy lets you work in a lower level programming data language that is not truly raw SQL so it can still adapt to the various different types of databases.<br>
It's mature and it's very fast it's been around for over 10 years some of the really hot spots are written in C so it's not some brand new thing it's been truly tested and is highly tuned.<br>
It's DBA approved, who wouldn't want that?<br>
What that mean is, by default SQLAlchemy will generate SQL statements based on the way you interact with the classes.<br>
But you can actually swap out those with hand optimized statements.<br>
So if the DBA says well, there's no way we're going to run this all the time you can actually change how some of the SQL is generated and run.<br>
Well, the ORM is not required I recommend it for about 80%, 90% of the cases.<br>
It makes programming much simpler, more straightforward and it much better matches the way you think about data in your Python application rather than how it's normalized in the database so it has a really, really nice ORM or lots of features and this is what we're going to be focusing on in this course.<br>
I'll also use this as the unit of work design pattern and so that concept is I create a unit of work I make, insert updates, delete, etc.<br>
all of those within a transaction basically and then at the end, I can either commit or not commit all of those changes at once.<br>
Cause this is an opposition to the other style which is called active record, where you work with every individual piece of data separately and it doesn't commit all at once.<br>
There's a lot of different databases supported so SQLite, Postgres, MySQL Microsoft SQL Server, etcetera, etcetera.<br>
There's lots of different database support.<br>
And finally, one of the problems that we can hit with ORMs is through relationships.<br>
Maybe I have a package and the package has releases.<br>
So I do one query to get a list of packages and I also want to know about the releases.<br>
So every one of those package when I touch their releases relationship, it will actually go back to the database and do another query.<br>
So if I get 20 packages back, I might do 21 overall database operations separately.<br>
That's super bad for performance.<br>
So you can do eager loading and have SQLAlchemy do just one single operation in the database that is effectively adjoined or something like that that brings all that data back.<br>
So if you know that you're going to work with the relationships ahead of time you can tell SQLAlchemy, I'm going to be going back to get these so also load that relationship.<br>
And these are just some of the reasons you want to use SQLAlchemy.
|
|
show
|
1:35 |
When you choose a framework whether that's for a database or a web framework it's good to know that you're in good company and that other companies and products have already tested this and looked around and decided Yep, SQLAlchemy is a great choice.<br>
So let's look at some of the popular deployments.<br>
Dropbox is a user of SQLAlchemy and Dropbox is one of the most significant Python shops out there.<br>
Guido van Rossum and some of the other core developers work there and almost everything they do is in Python.<br>
So the fact that they use SQLAlchemy that's a very high vote of confidence.<br>
Uber.<br>
Uber uses SQLAlchemy.<br>
Reddit.<br>
Reddit's interesting in that they don't use the ORM but in fact they use only the core.<br>
At least, wow, hey we're using only the core aspect of SQLAlchemy, that's pretty cool.<br>
Firefox, Mozilla, more properly is using SQLAlchemy.<br>
OpenStack makes heavy use of SQLAlchemy.<br>
FreshBooks, the accounting software based on, you guessed it, SQLAlchemy!<br>
We've got Hulu, Yelp, TriMet, that's the public transit authority for all of Portland, Oregon.<br>
The trains, the buses and things like that so they use that as well.<br>
So here are just a couple of the companies and products that use SQLAlchemy.<br>
There's some really high pressure some of these are under.<br>
You know if it's working for them it's going to work well for you, especially Reddit.<br>
Reddit gets a crazy amount of traffic, so pretty interesting that they're all using it and we'll see why in a little bit.
|
|
show
|
2:14 |
Before we actually start writing code for SQLAlchemy, let's get a quick 50,000 foot view by looking at the overall architecture.<br>
So when we think of SQLAlchemy there's really three layers.<br>
First of all, it's build upon Python's DB-API.<br>
So this is a standard API, actually it's DB-API 2.0 these days, but we don't have that version here.<br>
This is defined by PEP 249 and it defines a way that Python can talk to different types of databases using the same API.<br>
So SQLAlchemy doesn't try to reinvent that they just build upon this.<br>
But there's two layers of SQLAlchemy.<br>
There's a SQLAlchemy core, which defines schemas and types.<br>
A SQL expression language, that is a kind of generic query language that can be transformed into a dialect that the different databases speak.<br>
There's an engine, which manages things like the connection and connection pooling, and actually which dialect to use.<br>
You may not be aware, but the SQL query language that used to talk to Microsoft SQL server is not the same that used to talk to Oracle it's not the same that used to talk to Postgre.<br>
They all have slight little variations that make them different, and that can make it hard to change between database engines.<br>
But, SQLAlchemy does that adaptation for us using its core layer.<br>
So if you want to do SQL-like programming and work mainly in set operations well here you go, you can just use the core and that's a little bit faster and a little bit closer to the metal.<br>
You'll find most people, though when they're working with SQLAlchemy it will be using what's called an Object Relational Mapper, object being classes relational, database, and going between them.<br>
So what you do is you define classes and you define fields and properties on them and those are mapped, transformed into the database using SQLAlchemy and its mapper here.<br>
So what we're going to do is we're going to define a bunch of classes that model our database things like packages, releases users, maintainers and so on in SQLAlchemy and then SQLAlchemy will actually create the database the database schema, everything and we're just going to talk to SQLAlchemy.<br>
It'll be a thing of beauty.
|
|
show
|
3:10 |
When you're trying to model something like PyPI a website with a database the clearer of a picture you have the better you're going to be.<br>
So let's look around this running, finished version.<br>
Remember this is not even though it looks very much like what we built this one is actually the finished one that we're going to be sort of be aiming for.<br>
Alright, now we're not going to look at the code but we'll poke around what the web looks like and we could just as well look at the real one but let's look at this.<br>
So on any given package this is pulling up the package AMQP and apparently that's a low level AMQP client for Python.<br>
Okay, great, actually I've never used this.<br>
We have a couple of things going on here.<br>
We have the name of the package, the version bunch of different versions actually a description, right here.<br>
We actually have a release history so each package has potentially multiple releases.<br>
You can see this one had many different releases and we can pull up the details about different ones.<br>
Jump back there.<br>
We have downloads, we have information like the homepage.<br>
So, right over here we can go to GitHub apparently that has to do with Celery.<br>
We could pull up some statistics about it.<br>
It has a license, it has an author.<br>
So remember, up here we have a login and register so we could actually go login to the site or create an account just as a regular user and then we could decide as Barry Pederson apparently did, to just publish a package.<br>
And then there's a relationship between that user and this package as a maintainer and it's probably a normalization table.<br>
We also have a license, BSD in this case.<br>
If we want to model this situation in a relational database let's see how we do that.<br>
PyCharm has some pretty sweet tooling around visualizing database structure.<br>
So, here we're going to have a package and it's going to have things like a summary and a description and a homepage license, keywords, things like that.<br>
It has an author, but it's also potentially has other maintainers.<br>
So we have our users, name, email, password things like that.<br>
And then I don't have the relationship drawn in this diagram but there'll be a relationship between the user id and the user id and the package id and the package id there.<br>
So this is what's often referred to as a normalization table for many-to-many relationships so that's one part.<br>
And then the package, remember, it has releases.<br>
So here, each release has an id it has a major/minor build version a date, comments, ways to download that different sizes as it changes over time.<br>
We also have licenses, that relate back there and we have languages.<br>
So here, this is going to relate back say to that id right there.<br>
Finally, we're not going to track any of this but there actually are download statistics about this about downloading all these packages and the different releases and so on so we went ahead and threw that in there.<br>
So this is what we're going to try to build but we're not going to build it in the database.<br>
We're going to build it in SQLAlchemy and SQLAlchemy will maintain the database for us.<br>
Think the place to get started is packages so let's go on and do that.
|
|
show
|
8:31 |
Let's start writing some code for our SQLAlchemy data model.<br>
And as usual, we're starting in the final here's a copy of what we're starting with.<br>
This is the same right now, but we'll be evolved to whatever the final code is.<br>
So, I've already set this up in PyCharm and we can just open it up.<br>
So, the one thing I like to do is have all my data models and all the SQLAlchemy stuff in its own little folder, here.<br>
So, let's go and add a new directory called data.<br>
Now, there's two or three things we have to do to actually get started with SQLAlchemy.<br>
We need to set up the connection to the database and talk about what type of database is it.<br>
Is it Postgres, is a SQLite, is it Microsoft SQL Server?<br>
Things like that.<br>
We need to model our classes which map Python classes into the tables, right, basically create and map the data over to the tables.<br>
And then, we also need a base class that's going to wire all that stuff together.<br>
So, the way it works is we create a base class everything that derived from a particular base class gets mapped particular database.<br>
You could have multiple base classes, multiple connections and so on through that mechanism.<br>
Now, conceptually, the best place to start I think is to model the classes.<br>
It's not actually the first thing that happens in sort of an execution style.<br>
What happens first when we run the code but, it's conceptually what we want.<br>
So, let's go over here, and model package.<br>
Do you want to create a package class Package?<br>
Now, for this to actually work we're going to need some base class, here.<br>
But, I don't want to really focus on that just yet we'll get to that in a moment.<br>
Let's stay focused on just this idea of modeling data in a class with SQLAlchemy that then maps to a database table.<br>
So, the way it works is we're going to have fields here and this is going to be like an int or a string.<br>
You will have a created_date which, is going to be a datetime.<br>
We might have a description, which is a string, and so on.<br>
Now, we don't actually set them to integers.<br>
Instead, what we're going to set them to our descriptors that come from SQLAlchemy.<br>
Those will have two distinct purposes.<br>
One, define what the database schema is.<br>
So, we're going to set up some kind of column information with type equals integer or something like that.<br>
And SQLAlchemy will use that to generate the database schema.<br>
But at runtime, this will just be an integer and the creator date will just be a datetime, and so on.<br>
So, there's kind of this dual role thing going on with the type definition here.<br>
We're going to start by importing SQLAlchemy.<br>
And notice that that is turning red and PyCharm says, "you need to install this", and so on.<br>
Let's go make this little more formal and put SQLAlchemy down here's a thing and PyCharms like "whoa, you also have to install it here".<br>
So, go ahead and let it do that.<br>
Well, that looks like it worked really well.<br>
And if we go back here, everything should be good.<br>
Now, one common thing you'll see people do is import sqlalchemy as sa because you say, "SQLAlchemy" a lot.<br>
Either they'll do that or they'll just from SQLAlchemy import either * or all the stuff they need.<br>
I preferred have a little namespace.<br>
So, we're going to do, we want to come here and say sa.Column.<br>
And then, we have to say what type.<br>
So, what I had written there would have been an integer.<br>
And we can do things like say this is the primary_key, true.<br>
And if it's an integer you can even say auto_increment, true which, is pretty cool.<br>
That way, it's just automatically managed in the database you don't have to set it or manage it or anything like that.<br>
However, I'm going to take this away because of what we actually want to do is use this as a string.<br>
Think about the packages in PyPI.<br>
They cannot have conflicting names.<br>
You can't have two separate packages with the name Flask.<br>
You can have two distinct packages with the name SQLAlchemy.<br>
This has to be globally unique.<br>
That sounds a lot like a primary key, doesn't it?<br>
Let's just make the name itself be the primary key.<br>
And then over here, we're going to do something similar say, column, and that's going to be sa.DateTime.<br>
I always like to know almost any database table I have I like to know when was something inserted.<br>
When was it created?<br>
That way you can show new packages show me the things created this week or whatever order them by created descending, who knows.<br>
Now, we're actually going to do more with these columns, here.<br>
But, let's just get the first pass basic model in place.<br>
Well, it's going to have a summary.<br>
It's going to be sa.Column(sa.String we'll have a summary, also a description.<br>
We're also going to have a homepage.<br>
Sometimes this will be just the GitHub page.<br>
Sometimes it's a read the docs page, you never know but, something like that.<br>
We'll also have a docs page or let's say a docs_url.<br>
And some of the packages have a package_url.<br>
This is just stuff I got from looking at the PyPI site.<br>
So, let's review real quick for over here on say SQLAlchemy here's the name.<br>
And then what, we're going to have versions that's tied back to the releases, and so on.<br>
Here's the project description we have maybe the homepage we'll also have things like who's the maintainers what license does it have, and so on.<br>
So, let's keep going.<br>
Now, here's the summary, by the way and then, this is the description.<br>
Okay, so this stuff is all coming together pretty well.<br>
Notice over here, we have an author, who is Mike Bayer and we have maintainers, also Mike Bayer, right there and some other person.<br>
So, we have this concept of the primary author who may not even be a maintainer anymore and then maintainers.<br>
So, we're going to do a little bit of a mixture.<br>
I'm going to say, author name it's going to be one of these strings just embed this in here and it will do email so, we can kind of keep that even if they delete their account.<br>
And then later, we're going to have maintainers and we're also going to have releases.<br>
These two things I don't want to talk about yet because they involve relationships and navigating hierarchies, and all that kind of stuff we're going to focus on that as a separate topic.<br>
The last thing we want to do is have a license, here.<br>
And for the license we want to link back to a rich license object.<br>
However, we don't necessarily want to have to do a join on some integer id to get back just the name to show it there.<br>
It would be cool if we could use somehow use this trick.<br>
And actually, we can, we can make the name of the license the friendly name, be just the id, right.<br>
You would not have two MIT licenses.<br>
So, we'll just say this is an sa.Column(sa.String which, is an Sa.string.<br>
That's cool, right, okay, so here is our first pass at creating a package.<br>
So, I think this is pretty good.<br>
It models pretty accurately what you see over here on PyPI.<br>
There's a few things that we're omitting like metadata and so on, but it's going to be good enough for demo app.<br>
A couple more things before we move on here.<br>
One, if I go and interact with this and try to save it into create one of these packages and save it into the database with SQLAlchemy, a couple of things.<br>
One, it needs a base class.<br>
We're going to do that next.<br>
But, it's going to create a table called Package which is singular.<br>
Now, this should be singular, this class it represents a single one of the packages in the database when you work with it in Python but, in the database, the table represents many packages all of them, in fact.<br>
So, let's go over here and use a little invention in SQLAlchemy to change that table name but, not the class name.<br>
So, we'll come down here and say __tablename__ = 'packages', like so So, if we say it's packages that's what the database is going to be called.<br>
And we can always tell PyCharm that, that's spelled correctly.<br>
The other thing to do when we're doing a little bit of debugging or we get a set of results back in Python, not in the database and we just get like a list or we want to look at it it's really nice to actually see a little bit more information in the debugger than just package, package, package at address, address, address.<br>
So, we can control that by having a __repr__ and just returning some string here like package such and such like this.<br>
I'll say self.id.<br>
So, you know, if we get back, Flask and SQLAlchemy we'd say, angle bracket package Flask angle bracket package SQLAlchemy.<br>
So, that's going to make our debugging life a little bit easier as we go through this app.<br>
Alright, so not finished yet but, here's a really nice first pass at modeling packages.
|
|
show
|
1:51 |
Νow we defined our package class we saw that it's not really going to work or at least I told you it's not going to work unless we have a base class.<br>
So what we're going to to, is going to define another Python file here and this is going to seem a little bit silly to have such small amount of code in its own file but it really helps break circular dependencies so totally worth it.<br>
When I create a file here called model base.<br>
And you would think if we're going to create a class it would be something like this, SQLAlchemyBase.<br>
Then we would do stuff here right?<br>
That's typically how you create a class.<br>
But it's not the only way and it's not the way that SQLAlchemy uses.<br>
So what SQLAlchemy does is it uses a factory method to at run time, generate these base classes.<br>
We can have more than one.<br>
We can have a standard one.<br>
We can have a analytics database one.<br>
All sorts of stuff.<br>
So you can have one of these base classes for each database you want to target.<br>
And then factor out what classes go to what database based on like maybe the core database or analytics by driving from these different classes.<br>
We don't need to do that.<br>
We're just going to have the one.<br>
But it's totally reasonable.<br>
So let's go over here and say import sqlalchemy.ext.declarative as dec add something shorter than that.<br>
And then down here instead of saying the class then we say the dec.declarative_base().<br>
That's it, do a little clean up, and we are done.<br>
Here's our base class and now we can associate that with a database connection and these classes.<br>
So we just come over here in the easiest way and PyCharm is just to put it here and say Yes, you can import that at the top.<br>
So it adds that line rewrite there, and that's it.<br>
This class is ready to be saved to the database.<br>
We have to configure SQLAlchemyBase itself a little bit more.<br>
But package, it's ready to roll.
|
|
show
|
6:01 |
Before we can interact with our packages and query any or save them to the database or anything like that, we're going to need to well, connect to the database.<br>
And a lot of the connections and interactions with the database in SQLAlchemy they operate around this concept of the unit of work.<br>
Construct inside SQLAlchemy that represents the unit of work is called a session and it internally manages the connection.<br>
So with that in mind, let's go and add a new Python file called db_session.py.<br>
So in this file, we're going to get all the stuff set up so that you can ask it for one of these sessions and commit or rollback the session and so on.<br>
So we need to create two basic things.<br>
We need a factory, and I'll just say None for the moment and we need an engine.<br>
Now the engine I don't believe we need to share but this factory we're going to need to somehow keep this around right this is kind of we'll use the engine to get the factory and so on.<br>
So let's go and let's create a little function here called global_init(db_file: str) and we're going to use SQLite.<br>
It's the simplest of all the databases that are actual relational, you don't have to set up a server, it just works with a file but it's a proper relational database.<br>
The way we do that is we pass in a db_file which is a string.<br>
So what we want to do is work with this and see if, this has been called before we don't need to run it twice so we'll do something like this.<br>
We'll say first let's just make sure that's global factory - we'll say if factory: return.<br>
No need to call it twice, right?<br>
But, if it hasn't been called let's do maybe some validation here.<br>
We'll say if not db_file or not db_file.strip() it'll raise some kind of exception here, like right, something pretty obvious.<br>
You have to pass as a database file otherwise we can't work with it.<br>
Then we're going to get an engine here.<br>
The way we get the engine is from SQLAlchemy so we're going to have to import sqlalchemy.<br>
Maybe we'll stick with this as sa so here we just say sa.create_engine.<br>
Super simple.<br>
Notice the signature here though *args, **kwargs".<br>
I utterly hate this pattern it means, you can pass anything and we're not going to help you or tell you what could possibly go in there.<br>
In fact, there is a known set of things you can pass in here, so they should just be keyword arguments.<br>
Well, anyway, that's the way it goes.<br>
So, we're going to pass a couple of things.<br>
We need to come up with a connection string and when you're working with SQLAlchemy what you do is you specify the database schema or the type of database that's part of the connection string.<br>
So we'll say sqlite:/// and then we just add on the DB file like this.<br>
Maybe just to be safe we'll say "strip", just in case.<br>
That seems like that's a pretty good one and then here we'll just pass the connection string as a positional parameter, and then we also may want to set this, so I'll go ahead and like prime the pump for you.<br>
I'll say "echo=false".<br>
If you want to see what SQLAlchemy is doing what queries it's sending, what table create statements it's sending, stuff like that you set this to "true", all the SQL commands sent to the database are also echoed out both just standard out, and standard error, I believe.<br>
But I'm not going to show that, but just having this here so you know you can flip that and really see what's going on here.<br>
So we're going to set the factory here but what we need is we actually need import sqlalchemy.orm as orm So we come in here and we say orm.sessionmaker session...<br>
maker.<br>
So this is a little bit funky, but you call this function and it creates a callable which itself when you call it, will create these units of work or these sessions.<br>
So we got this little bit of a funky thing going on here, but then we also come in here and we say "bind=engine".<br>
So when the session is created it's going to use the engine which knows the database type and the database connection, and all that kind of stuff.<br>
And that's pretty much it!<br>
We're going to use this factory after everything's been initialized.<br>
We're actually going to do a couple of iterations on this file to make it a little bit better and better as we go, but let's not get ahead of ourselves.<br>
I think this pretty much does it so let's go ahead and call this over here.<br>
Let's go - here we are, register_blueprints like, setup_db(), let's have just a function down here...<br>
So let's go ahead and create a data folder not a data data, but a DB folder.<br>
In here is where we're going to put our database file.<br>
So what we need to do is work with the OS module and we're going to actually figure out where this is.<br>
So we want to say "durname", "pathoutdurname" and we're going to give it the dunder file for apps.<br>
So that's going to be the directory that we're working in so right now we're this one, and then we need to join it with DB and some file name.<br>
Let's say "DBFile" it's going to be "OS.path.join" this directory with "db" with some database file.<br>
Let's just call it "pypi.sqlite".<br>
The extension doesn't matter but, you know maybe it'll kind of tell you what's going on there.<br>
And then we can just go up here.<br>
import pypi_org.data.db_session as db_session and come down here and just call our global_init().<br>
Pass the db_file, and that's it we should be golden.<br>
Let's go ahead and run it just to make sure everything's hanging together.<br>
Ha ha, it did, it worked!<br>
Off it goes.<br>
There it is up and running.<br>
Super super cool.<br>
Did anything happen here?<br>
Probably not yet, no, nothing yet.<br>
But pretty soon when we ask SQLAlchemy to do something, like even just verify that the tables are there, you'll see SQLite will automatically create a file there for us.<br>
Okay, great, looks like our database connection is all set up and ready to go.
|
|
show
|
6:26 |
Well, we've got our connection set we've got our model, at least for the package, all set up we've got our base class.<br>
Let's go ahead and create the tables.<br>
Now, notice we've got no database here even though over in our db.session we've talked to the database.<br>
We haven't actually asked SQLAlchemy to do any interaction with it, so nothing's happened.<br>
One way we could create the tables is we could create a file, create a database and open up some database design tools and start working with it.<br>
That would the wrong way.<br>
We have SQLite.<br>
We've already defined exactly what things are supposed to look like and it would have to match this perfectly anyway.<br>
So, why not let SQLAlchemy create for us?<br>
And, you'll see it's super easy to do.<br>
So, you want to use SQLAlchemyBase.<br>
Remember, this is the base class we created.<br>
Just import that up above.<br>
This has a metadata on there.<br>
And here we can say create_all.<br>
Now, notice there wasn't intellisense or auto complete here.<br>
Anyway, some stuff here, but create_all wasn't don't worry, it's there.<br>
So, all we got to is pass it, the engine.<br>
That's it!<br>
SQLAlchemy will go and create all of the models that it knows about it will go create their corresponding tables.<br>
However, there's an important little caveat that's easy to forget.<br>
All the tables or classes it knows about and knows about means this file has been imported and here's a class that drives from it.<br>
So, in order to make sure that SQLAlchemy knows about all the types we need to make sure that we import this.<br>
So, because it's only for this one purpose let's say from pypi.org.data.package import package, like this.<br>
Now, PyCharm says that has no effect you don't need to do that.<br>
Well, usually true, not this time.<br>
Let's say, look, we need this to happen and normally you also put this at the top of the file but I put it right here because this is the one and only reason we're doing that in this file is so that we can actually show it to the SQLAlchemyBase.<br>
So, first of all, let's run this and then I'll show you a little problem here and we'll have one more fix to make things a little more maintainable and obvious.<br>
So, notice over here db folder empty.<br>
We run it, everything starts out well and if we ask it to refresh itself, oo, look!<br>
There's our little database, and better than that it even has a database icon.<br>
It does not have an icon because of the extension it has an icon 'cause PyCharm looked in the binary files and said that looks like a database I understand.<br>
So, let's see what's in there.<br>
Over here we can open up our database tab.<br>
This only works in Pro version of PyCharm.<br>
I'll show you an option for if you only have the Community in a moment.<br>
If we open this up, and look!<br>
It understands it, we can expand it and it has stuff going on in here.<br>
If, if, if, if, this is a big if so go over here, and you say new data source, SQLite by default, it might say you have to download the drivers or maybe it says it down here it kind of has moved around over time.<br>
Apparently, there's an update, so I can go and click mine but if you don't go in here and click download the drivers PyCharm won't understand this so make sure you do this first.<br>
Cool, now we can test the connection.<br>
Of course, it looks like it's working fine because we already opened it and now here we have, check that out!<br>
There's our packages with our ID which is a string create a date, which is the date, time all that good stuff, our primary keys, jump to console and now, I can say select star from packages, where?<br>
All through email, homepage, ID, license there's all the stuff, right?<br>
Whatever, I don't need a where clause and actually it's not going to be super interesting because it's empty.<br>
Obviously, it's empty, we haven't put anything in there but, how cool!<br>
We've had SQLAlchemy generate our table using the schema that we defined over here and it's here up in the database tools, looking great right?<br>
Well, that pretty much rounds it out for all that we have to do.<br>
We do have some improvements that we can make on this side but that's a pretty awesome start.<br>
I did say there was another tool that you can use.<br>
So, if you don't have the Pro version of PyCharm you can use a DB Browser for SQLite that's free.<br>
And if I go over to this here I can open up the DB Browser here and say open database, and give it this file.<br>
And check it out, pretty much the same thing.<br>
I don't know really how good this tool is I haven't actually used it for real projects but it looks pretty cool, and it definitely gives you a decent view into your database.<br>
So, if you don't have the Pro version of PyCharm here's a good option.<br>
Alright, so pretty awesome.<br>
I did say there's one other thing I would like to do here just for a little debugging now let's just do print connecting to DB with just so we see the connection string when it starts up so you can see, you know a little bit of what is happening here, and that will help.<br>
And the other thing is, I said that this was a little error prone, why is this error prone?<br>
Well, in real projects you might have 10, 15, 20 maybe even more of these package type files, right?<br>
These models.<br>
And if you add another one, what do you have to do?<br>
You have to absolutely remember to dig into this function, and who knows where it is and what you thought about it, and how long?<br>
And make sure you add an import statement right here and if you don't, that table may not get created it's going to be a problem.<br>
So, what I like to do as just a convention to make this more obvious is create another file over here called __all_models and basically, put exactly this line right there.<br>
And we'll just put a note, and all the others all the new ones.<br>
So, whenever we add a new model just put it in this one file.<br>
It doesn't matter where else this file gets used or imported, or whatever, if it's here, it's going to be done.<br>
So, to me, this makes it a little cleaner then I can just go over here and just say import __all_models and that way, like, this function deep down in it's gut, doesn't have to change.<br>
It should still run the same, super cool.<br>
Okay, so that's good.<br>
I think it's a little more cleaned up.<br>
We've got our print statement so we got a little debugging and then we've got our all models so it makes it easier to add new ones.
|
|
show
|
5:00 |
Let's return to our package class that defines the table and the database.<br>
I said there was a few other things that we needed to do and well, let's look at a few of the problems and then how to fix them.<br>
created_date.<br>
created_date is great.<br>
What is it's value if we forget to set it?<br>
Null.<br>
That's not good.<br>
We could say it's required and make us set it but wouldn't it even be better if SQLAlchemy itself would just go right now, the time is now and that's the time it's created on first insert?<br>
Super easy to do that.<br>
But, we've got to explicitly say that here.<br>
We're here in the emails.<br>
Maybe we want to be able to search by author/email.<br>
So, we might want to have an index here so we can ask quickly ask the question: show me all the packages authored by the person with such and such email.<br>
Boom.<br>
If we had a index, this could be super, super fast.<br>
The difference in in terms of query time for a query with an index and without an index with a huge amount of data can be like a thousand time slower it's an insane performance increase to have an index.<br>
Helps you sort faster, it helps you query faster and so on.<br>
So, we're going to want to add an index on some of these.<br>
And maybe the summary is required, but the description is not required.<br>
So, we would like to express that here as well.<br>
So, SQLAlchemy has capabilities for all these.<br>
So, let's start with the default value.<br>
So, pretty easy, we're going to set defaults here and it could be something like 0 or True or False if that made sense, it doesn't for datetime.<br>
Well, what would be the value for a datetime?<br>
Well, let's use the datetime module and we can import that at the top.<br>
And use datetime.now.<br>
Now, notice if I just press end and then enter bicharm is super helpful and it puts parenthesis here.<br>
That is a horrible thing that happened.<br>
What this will do is take the current time when the application starts and say well that time is the default value.<br>
No no.<br>
That's not what we want.<br>
What we want, is we want this function now to be passed off in any time SQLAlchemy is going to put something in the database, it goes, oh, I have a default value which is a function so let me call that now to get the value.<br>
So, that will do the trick of getting the insert time exactly as you want.<br>
Here we can say, nullable=False you can say, nullable=True.<br>
Not all databases support the concept of nullability like, I think SQLite doesn't, but you don't want to necessarily guarantee that you're always working with that, right?<br>
We may also want to say, all like, all the packages or the top ten packages are the most recent ones.<br>
And, for that you might want an index cause then that makes it really fast to do that sort, so we can say index=True.<br>
And that's all you got to do to add an index it's incredible.<br>
We also may want to ask, you know, show me all the packages this person has written.<br>
So, then we'd say index is true, that'll be super fast.<br>
Also, you might ask, what are all the packages or even, how many of them are there that have say the MIT license?<br>
And, then you could do a count on it or something, right?<br>
So, this index will make that query fast.<br>
These we'll deal with when we get to relationships.<br>
But, these simple little changes here will make it much, much better and this is really what we wanted to define in the beginning, but I didn't want to overwhelm you with all the stuff at the start with.<br>
All right, so we have our database over here.<br>
Let's go and you know, and we have it here as well.<br>
Let's go and run the app, rerun it see if everything's working and if we just refresh this what's going to happen.<br>
sad face, nothing happened.<br>
Where are my indexes?<br>
Where is my null, well, nullability statements things like that.<br>
This is a problem, this is definitely a problem.<br>
The reason we don't see any changes here is that SQLite will not create or modifications to a table.<br>
It'll create new ones, great new tables.<br>
If I add a new table it'll create it like uh release it or something, you would see it show up when I ran it.<br>
But, once it exists, done.<br>
No changes, it could lose data or it could make other problems, so it's not going to do it.<br>
Leader, and this is very common, you want this but SQLAlchemy won't do it.<br>
We're going to later talk about something called Alembic which will do database migrations and evolve your database schema using our classes here.<br>
But, we're not there yet.<br>
We just are trying to get SQLAlchemy going.<br>
So, for now, what do we do, how do we solve this?<br>
We could either just delete this file, drop that table and just let it recreate it, right, we don't really have any data yet.<br>
When you get to production migrations, but for now just super quick, let's just drop that table.<br>
I'll rerun the app, refresh the schema, expando and look at that, here's our indexes and author/email creator date and license, here's our uniqueness constraint on the id which comes with part of the primary key.<br>
You can see those, blue things right there those are the indexes.<br>
So, we come over here and modify the table, you can see here's your indexes and your keys and so on.<br>
Cool, huh?<br>
All we need to do is put the extra pieces of information on here and when we enter one of these packages we'll get in and out and we'll have an index for it and so on.<br>
Super, super cool.
|
|
show
|
4:24 |
Now for the rest of the tables I don't want to go and build them piece by piece by piece.<br>
That'll get pretty monotonous pretty quickly.<br>
We did that for package and it's quite representative.<br>
So let's just do a quick tour of the other ones I just dropped them in here for you.<br>
So we have download and of course change the database name the table name to downloads, lower case.<br>
And again we have an id, this one is an integer which is primary key in auto-incrementing, that's cool.<br>
created_date, most of our tables will have this because of course you want to know when that record was entered.<br>
You almost always at some point want to go like actually, when did this get in here?<br>
Or all of these or something like that.<br>
So always have that in there.<br>
And then this is going to represent a download of a package like pip install flask, right, like when that happens if that hits the server we want a analytical record of that.<br>
So we want to know what package and what release we hit and we may want to query or do reporting based on that so we'll use an index here.<br>
We also might just want to track the IP address and user agent so we can tell what it is that's doing the install.<br>
That one's pretty straightforward, what's next?<br>
Languages, again wrote in this trick where the name of the language are like, Cython or Python 3 or whatever.<br>
That's going to be the primary key 'cause it was very unique and then we also have when it was put there and a little description about what that means.<br>
Licenses like MIT or GPL or whatever, same thing.<br>
One more, one more ID is the name.<br>
Or you can avoid, join potentially and get things a little quicker that way.<br>
Little description and create a date, always want that.<br>
This is an interesting one here this is for our many to many relationship.<br>
We have users and we have packages and a user can maintain many packages and a package be maintained by multiple users.<br>
So here's our normalization table we have the user ID and package ID and these are primary keys.<br>
Possibly we should go ahead and set up these as foreign key relationships, but I didn't do it.<br>
We'll do that over here, so this one hasn't changed we still got to add our relationship there at the bottom.<br>
Our releases, this one is going to have a major, minor and build version all indexed.<br>
Just an auto-increment and primary key.<br>
Maybe some comments in the size of that release and you know, stuff like this.<br>
And finally we have our user these are the users that log into our site auto-incrementing ID, the name, it's just a string email.<br>
It has an index we should also say this probably is unique.<br>
It's true as well 'cause we can't have two people with the same email address, that would be a problem when they want to reset their password.<br>
We also, speaking of passwords, want to store that but we never, never, never store just the password.<br>
That is a super bad idea.<br>
We're going to talk about how we do this but we're going to hash the password in an iterative way mix it in with some salt and store it here.<br>
So to make that super clear this is not the raw password but that is something that needs transforming we put hash password.<br>
But we also want the created date, maybe their profile and their last login just so we know like who the active users are and whatnot.<br>
That's all of the tables, or the classes that we've got.<br>
If you look over here, I've updated this.<br>
And guess what?<br>
We don't need to go change like our db_session or whoever cared about importing all these things.<br>
It's all good.<br>
Notice also that I put this in alphabetical order.<br>
That means when you go add a new class or table it's super easy to look here and see if it's listed and if it's not, you know put it where it goes alphabetically.<br>
It'll help you keep everything sane.<br>
So let's see about these tables over here we have not run this yet.<br>
Notice we just have packages, but if we rerun the app what's going to happen?<br>
It did not like that, our package singular.<br>
Let's try again.<br>
Here we go, and now if we refresh, resynchronize boom, there they all are.<br>
So we see our downloads and it has all the pieces and the indexes say on like, package ID right there our license and so on.<br>
Everything's great.<br>
So SQLAlchemy did create the new tables it did not make any changes to packages.<br>
Luckily we haven't actually changed anything meaningful about packages, so that's fine.<br>
If we did we'd have to drop the table or apply migrations, right.<br>
But it's cool that at least these new tables we're defining they do get created there and then yeah it's pretty good.<br>
All right so I think we're almost done we're almost done we just have a few more things to tie the pieces together to relationships and our data modeling will be complete.
|
|
show
|
7:16 |
The last thing we have to do to round out our relational database model is to set up the relationships.<br>
So far we've effectively have just a bunch of tables in the database with no relationships.<br>
We're not really taking advantage of what they have to offer so in order to work with these let's go up here I'm going to do a little bit more of an import the ORM aspect to we'll talk about relationships and Object Relational Mappers and so on.<br>
So over here, what we want to do is we want to have just a field releases and when we interact with that field we would like it to effectively go to the database do a join to pull back all the releases through this relationship that are associated with this package.<br>
So basically where the package ID on the release matches whatever ideas here.<br>
So the way that we're going to do this is we're going to go over her and say this is an orm.relation relation just singular like that and then in here we talk about what class in SQLAlchemy terms this is related to.<br>
It's going to be a release, that's good and that alone would be enough but often you want the stuff you get back through this relationship to have some kind of order.<br>
Wouldn't you rather be able to go through this lesson say it's oldest to newest or newest to oldest releases?<br>
That would be great if you had to do no effort to make that happen right?<br>
Well guess what, we can set it up so there's no effort so we can say, order_by= Now we can put one of two things here.<br>
We could say I want to order my one column on the release class so we'll just say release and import it at the top.<br>
You could say we want to say accreted data descending like this and that would be a totally fine option but what we want to do is actually show, if we go over here we want to order first by the major version then the minor version, then the build version.<br>
All of those descending so in order to order by multiple things, we're going to put a list right here like so and let's say we're going to order it by major version minor version, and build version.<br>
All right so that means we get it back it's going to go to the database and have the database do the sort.<br>
Databases are awesome at doing sorts especially with things with indexes like these three columns right here have.<br>
Okay so that's pretty good and we're also going to have a relationship in the reverse direction so package has many releases.<br>
Each release is associated with one package.<br>
So over here when I have a package for a moment I'm going to leave it empty but we're going to have this field right here.<br>
So what we can do is we can say is back populates package what does that mean?<br>
That means when I go here and I get these all filled out and I iterate over my get one of the releases off maybe I hand it somewhere else somebody says .package on that release it will refer back to the same object that we originally had that we accessed it's here so it kind of maintains this bidirectional relationship behavior between a package, it's releases and a given release and it's packaged automatically so might as well throw that on there.<br>
All right this one's looking pretty good let's do the flip side.<br>
How does it know that there's some relationship?<br>
I just said this class is related to that boom done.<br>
Well that's not super obvious what this is.<br>
So what we're going to do is like standard database stuff is we're going to say there's a package_id here and this is a field or column in the release table to maintain that relationship and this will be a sa.Column, SQLAlchemy Column and what type is it going to be?<br>
It has to correspond exactly to the type up there.<br>
Okay so this has to be SQLAlchemy.string but in addition that it'll be SQLAlchemy.ForeignKey.<br>
So it's a little tricky to keep these things straight in your mind but sometimes we speak in terms of classes sometimes we speak in terms of databases.<br>
Over here I said the class name for a relationship but in this part, the foreign key you speak in terms of the databases, it'll be packages.id lowercase right that's this thing we're referring to that and the ID.<br>
So here we're going to have that foreign key right there and then this we can set that up to be a relationship as well.<br>
So again we got to get ahold of our orm orm.relation and it's going to relate back to package.<br>
Okay, I think that might do it.<br>
We're going to run and find out but we have it working in both directions so here we can go to the releases and then we can go, this will establish that relationship and this will be that referring the thing that refers back to it.<br>
Now we've already created those tables so in order for this to have any effect we have to drop those in this temporary form or member migrations later but not now.<br>
All right, let's run it and see if this works.<br>
All right, it seems happy that's a pretty good sign SQLAlchemy's pretty picky so we go over here there really shouldn't be anything we notice about this table but here we should now have a package and notice in that blue key, even right here there's a foreign key relationship if we go back and interact with this, say modify table we now have one foreign key from package_id which references packages ID, that's exactly what we wanted.<br>
Now we don't have any data here yet so this is not going to be super impressive but let me show you how this will work.<br>
Imagine somehow magically through the database through a query which we don't have anything yet but if we did, I'm going to come over here I'm going to go give me a package, right?<br>
Now I'm just allocating a new one but you could do a query to get to it right?<br>
So then I could say print P., I don't know what ID would be the name and then I could say print our releases and say for r in p.releases that will print out, here we go we go through and print them out.<br>
All right and we would only have to do one query except when explicit query here to get that and then down here, once it's back this would go back to the database potentially depending how we define that query and then do a query for all the releases ordered by the way we said and then get them back, that's pretty cool.<br>
Maybe like notice it says you can't iterate over this it's kind of annoying, let's see if I can fix this to say this is a list of release.<br>
Import that from typing, all right.<br>
So now it's happy and if I say r.<br>
notice that so maybe this is a good idea, I'm not sure if it's going to freak it out or not but I'll leave it in there until it becomes a problem.<br>
All right, let's actually just run it real quick make sure this works.<br>
Yeah that worked, didn't crash when we did that little print out.<br>
There was nothing to print but still, pretty good.<br>
So those are the releases.<br>
Once we get some data inserted you'll see how to leverage them even for inserts they're pretty awesome.
|
|
show
|
4:15 |
Before we actually start using SQLAlchemy to insert data and query data, and so on let's talk about some of the core concepts we've seen and some of the fundamental building blocks for modeling with SQLAlchemy.<br>
So we started with the SQLAlchemyBase.<br>
Remember, the idea was every class we're going to store in the database derived from this dynamically defined SQLAlchemyBase class.<br>
You can call it whatever you want.<br>
I like SQLAlchemyBase.<br>
But there's other, you know, it's just a variable.<br>
Name it as you like.<br>
So I want to create this singleton base class to register the classes and type sequence on the database.<br>
Remember, there's one and only one instance of this SQLAlchemyBase shared across all of the types per database.<br>
So for example, we're going to have a package to release a new user, they all derive from this one and only one SQLAlchemyBase type here.<br>
To model data in our classes, we put a bunch of classable fields here, ID, summary, size homepage, and so on, and each one of them is a Column.<br>
SQLAlchemy.Column and they have different types like integer, string, and so on.<br>
We can see some of them are primary keys and even if it's an integer, they can even be auto-incrementing, primary keys which is really, really nice.<br>
And we can also have relationships like we do between package and releases.<br>
One really nice feature of databases is they have default values.<br>
We saw with our auto-incrementing ID our primary key, we don't have to set it.<br>
The database does that for us.<br>
So here we can pass datetime.now, the function not the value, the function, and then it's going to call that function now whenever a row is created and set that value to be, well, right now.<br>
That's super nice.<br>
We can also do that up here with more complex expressions.<br>
So in the bottom one, we literally passed an existing function, datetime.now but above, we want to define this default behavior in a more rich way.<br>
So we're passing our very own lambda expression that takes the UUID for identifier converts it to a string, and then drops the dashes that normally separate it into just one giant scrambled alphanumeric super thing.<br>
You can create these default values by passing any function, a built-in one or one of your own making.<br>
We also want to model keys and indexes.<br>
So primary keys automatically have indexes.<br>
We don't have to do anything there.<br>
Let's our uniqueness constraint as well as indexes.<br>
This created one, maybe we want to sort by the newest users, for example.<br>
Well, if we're going to do that, we very much want to put an index on that.<br>
As I pointed out, indexes can have tremendous performance benefits, it's totally reasonable to have a thousand times difference performance in a query, if you have tons of data on whether you have an index or not.<br>
Indexes do slow write time but certainly in this case the rate of user creation versus querying and interacting with them is, it's no comparison, right?<br>
We're creating far fewer users, probably than we are querying or interacting with them.<br>
We can also specify uniqueness we didn't do that in our example.<br>
We can say this email, we can't have two users with the same email, emails are very often used to reset your password and if you have two users who's going to get their password reset, all of 'em?<br>
One of 'em, who knows, none of 'em?<br>
So you might want to say there's a uniqueness constraint on the email to say only one user gets to use a particular email and that's super easy to do by just saying unique=True.<br>
Finally, once all of the modeling is done we have to actually create the tables.<br>
Now turns out that that's super easy.<br>
We import all the packages, get the connection string and we can create an engine based on the connection string and then we just go to SQLAlchemyBase to it's meta-data and say create underscore all and pass the engine, boom, everything is done.<br>
Remember though, this only creates new tables it does not modify existing ones so if you need to modify it wait till we get to the alembic chapter, the migration chapter or do it yourself or if you're just in development mode maybe deleting it and just letting it recreate itself that might be the easiest thing, that's what we did.
|
|
|
52:56 |
|
show
|
2:18 |
In this chapter, we're going to look at actually using SQLAlchemy.<br>
Previously we had modeled all of our data but we didn't do anything with it.<br>
We didn't do any insert queries, updates none of that.<br>
That's what we're going to do now.<br>
And to get started we're just going to jump right into writing some code.<br>
And so I just want to point out we are now in Chapter 10 Using SQLAlchemy, working on the final bits here.<br>
So let's switch over to PyCharm grab our new chapter and get going.<br>
This is the code from before, just carrying on.<br>
And what we're going to do is we're going to actually have over here a new directory called bin.<br>
Now, this is just a convention that I use.<br>
I've seen it in other places as well and this bin folder comes along with our website for little admin tasks and scripts that we need to run and so on.<br>
It's not directly involved in running the site but more like maintaining the site.<br>
So, for example we're going to do some importing of data.<br>
And to do so, we're just going to write some scripts here.<br>
They don't actually run normally but they're going to run here.<br>
Let's go over and add a Python file called basic_inserts.py.<br>
We're going to take two passes at showing how to insert data.<br>
First, we're going to write some example data just standard make-up stuff.<br>
And then I'm going to show you I've actually got a hold of the real PyPI data for the top 100 packages using their API and we're going to insert that.<br>
Turned out that's super, super tricky.<br>
There's lots of little nuances and typecasting and all that kind of stuff we have to do to make it work just right.<br>
We're not going to do that first we're going to do like a simple example and then I'll show you the program that'll actually generate our real database.<br>
So, here it is.<br>
We already have our database right here and if we look at it we'll see that we have our packages and releases put together.<br>
And, of course, there are the interesting ones.<br>
Actually, I'll go over here and show you a little more.<br>
Show you a visualization pop-up.<br>
It's kind of a cool feature of PyCharm.<br>
So we have our packages and this relationship between the releases.<br>
That's probably the most interesting part of our database.<br>
We didn't actually set up save, like the maintainers and what not here.<br>
This should maybe have, like some relationships and so on but we didn't set up all the relationships for our data model, just the really important ones.<br>
So, we're going to focus on just those two tables.
|
|
show
|
1:36 |
Now what we're going to need to do is work with our SQLAlchemy engine and the factory and the connections and all that.<br>
And remember in order for that to work we have to go to our db_session.<br>
We have to initialize it.<br>
So let me just copy over a function here.<br>
This was just like the one we worked with before.<br>
So we're going to import os.<br>
import pypi.org and we can say that's spelled correctly.<br>
And we also want our db_session like so.<br>
And now it has this global_init.<br>
So, we just have to call this somewhere.<br>
Before we try to do anything, and just to remind you over here this sets up the connection string initializes the engine, and most importantly initializes this factory right here.<br>
So, what we're going to do is we're going to have a def main.<br>
Here this is going to be like our main startup.<br>
And in here we'll call global_init for the database.<br>
And we'll use our main convention at the bottom and just run the main function.<br>
Here we go.<br>
I guess we can go ahead and test it and see that it runs.<br>
Oh, it didn't like that.<br>
Let's just use this in here we'll just go up one.<br>
That should do.<br>
Okay, great.<br>
So here we're connecting to final, dah dah dah.<br>
It might even be worthwhile to say os.path.abspath.<br>
Here we go.<br>
Now we have the absolute path, in looks right to me.
|
|
show
|
1:33 |
Okay off to a good start and we no longer need this so this is good.<br>
Now what we need to do, let's just say while true we're going to insert a package.<br>
We'll write a little function that will does that.<br>
So down here, this can just enter package.<br>
So how do we insert a package and maybe some releases that are related to it?<br>
Well, how would you create a package if you forgot about the database and you only thought in classes and objects?<br>
You would say this p = Package(), like so, and you would import that from pypi_org.data.package.<br>
You would set some properties like the id might be, and we could ask the user input package id, name and we could do a strip in the lower just to make sure that is all consistent.<br>
What else do we need to set?<br>
Well, let's go actually look at our package real quick here.<br>
And we can say, what do we need to set?<br>
Well this, sometimes we don't have to set the primary key.<br>
This one is not auto_incrementing so we do.<br>
This one has a default value so that's solid.<br>
These could all be set, the releases we'll deal with in a second.<br>
So let's go over here and just put a little long comment string to show us what we got to do.<br>
So it'll be p.summary = input("Package summary: ").strip() For description I think we'll just ignore that all of those we can ignore.<br>
Let's just put author name and license.
|
|
show
|
3:36 |
Classes model records in our database so all we have to do is go and save this back into the database and that's where this unit of work in the factory from db_session, comes from.<br>
So what we'll do is we'll say session equals db_session.factory.<br>
So, this is pretty cool.<br>
If we come over here and say db_session.<br>
notice all the cool things you can do?<br>
No, no you don't.<br>
There's none.<br>
If I actually set the type here.<br>
I'm not going to leave it like this but, if I were to import sqlalchemy.orm like this, and set it to a session all of a sudden, we have, oh look autoflush, and commit, and bind and rollback, and all the database things and add, and so on.<br>
Let's go ahead and get this set up real quick and then I'll show you what we'd actually do.<br>
We'll make this a little bit nicer.<br>
There's some stuff down the road.<br>
We're going to need to make this better anyway.<br>
All right, so, we're going to do some work and then, the way this unit of work works is you say, I'm going to make a bunch of changes in this area create the session, make a bunch of changes.<br>
No interaction with the database will have happened until line 26, when we call commit or, if we had entered some kind of query but we're not querying, we're only inserting.<br>
Let's do this.<br>
Let's come over here, and we'll say session.add.<br>
That's it.<br>
If we run this, it's going to insert it right away but let's add a couple of releases.<br>
So we'll say, r., r is a release.<br>
I'm going to import that.<br>
Let's go over to releases and see what we got to set.<br>
id auto_increment in, we don't need to set.<br>
Those, we should definitely set.<br>
created_date is its own thing.<br>
So, let's just do those three and maybe the size.<br>
So, we'll say r.majorversion equals...<br>
Now, we're just going to assume that they know how to type an integer and, if they don't, well, it's going to crash, but it's OK.<br>
It's just a little example.<br>
Minor, and build.<br>
And, last one, will be size in bytes.<br>
OK.<br>
Looks pretty good.<br>
Now, how do we let's do a quick print.<br>
Release one, something like that and let's do it again for release two.<br>
So there's actually more than one release per package, OK?<br>
How do we associate this release with this package?<br>
There's a lot of ways.<br>
We could add each release separately.<br>
So we could say session.add each time and this, and then commit it as long as we had set r.package_id = p.id.<br>
Now actually, in this case, this would be fine but normally, windows are auto-incrementing IDs.<br>
You need to first save the package and then you can set it, and it gets real tricky, right?<br>
Like, it's not super simple to do this but, what is super simple is to work with this relationship over on packages.<br>
So, let's go to here, package.<br>
Remember this, releases?<br>
It's a relationship but it kind of acts like a list.<br>
Let's just try that.<br>
What happens if I say, p.releases.append as if it were a list.<br>
You know that's going to work.<br>
It is, it's awesome.<br>
OK, so we're going to append those and we're never going to tell the session to add it but it's going to traverse the relationships and go, I see you have other records to insert into other tables, and relationships to set up.
|
|
show
|
2:34 |
I think we're ready to try this.<br>
Let's go ahead and run this and see what happens.<br>
Off it goes, package name let's go with SQLAlchemy to start.<br>
Summary is ORM4 for Python.<br>
Author is Mike Bayer.<br>
License, let's just guess, it's MIT.<br>
The first version is 123.<br>
And it's that many bytes.<br>
The second one is going to be 2.0.0 and it's a little bit bigger, like so.<br>
So that inserted the first one.<br>
Let's do one more and say Flask, Microframework for Python.<br>
Let's go with Armand and David, right?<br>
Armand, originally, David Lord these days.<br>
Let's just say this is BSD.<br>
I have no idea what it is.<br>
And it's 1.0.0 and 1.0.02.<br>
There we go, that one can be bigger than one byte.<br>
All right, I think we're good.<br>
Let's go ahead and stop this.<br>
Notice, there were no crashes.<br>
That's pretty killer already.<br>
That's a good chance that something worked But let's go look in the database.<br>
So if I go over to Packages and I say Jump to Console and say, select * From Packages.<br>
Moment of truth.<br>
Tada!<br>
There they are, how awesome?<br>
We didn't set some of the values but we did set many of them.<br>
You can see everything we typed in there.<br>
Pretty awesome, isn't it?<br>
What about releases?<br>
Run those, look at that.<br>
There they are!<br>
And you can see what package they come from SQLAlchemy or Flask.<br>
That's really cool and that's actually the relationship.<br>
So I could go over here and, say, where package ID equals SQLAlchemy?<br>
Is it 1?<br>
I don't think it's what it equals.<br>
Here we go.<br>
So, we can go do the query for that and this is when we actually go back and do queries with SQLAlchemy and we touch that releases folder.<br>
It's going to do, basically, this query but it's also going to add an order by major version descending.<br>
And then minor and then whatever but this should be enough.<br>
There we go.<br>
So we'll get them back exactly in the order you would want them.<br>
All right, so this is how we insert data.<br>
Super, super simple, right?<br>
We go and just treat these more or less like objects.<br>
We create them, we set their properties we click them together through their relationships and we add them to the session, create a session and add it.
|
|
show
|
1:20 |
I guess, real, real quick, let's make this a little bit nicer.<br>
I don't like that this doesn't give us much information about what comes out of it and it's not super easy to get that to happen, so let's do this.<br>
Let's define a function called create_session().<br>
That's more obvious.<br>
And it's going to return a session object that comes from sqlalchemy.orm and for now it's just going to say return __factory(), like this.<br>
Let's tell it that this is global, like that.<br>
Okay, this is pretty good.<br>
But of course when we now go to db_session.<br>
Uou still see it.<br>
You still see it at the top and I'd kind of like that to not be the case so let's refactor rename this to double underscore and you see it renamed it everywhere.<br>
And now, is it gone?<br>
Ah, yeah.<br>
We just have create_session and global_init.<br>
So where are we doing this factory we don't need any of this stuff anymore.<br>
That can all be gone, we just say = DBsession.createsession the type annotation on the return value there should be enough so that when we say session.<br>
Boom, there it all is.<br>
Okay, that's it.<br>
We're all good to go.<br>
We clear the session, we make the changes we call commit, beautiful.
|
|
show
|
5:09 |
Now you've seen how to insert data with sequel Commie.<br>
We're going to insert the actual real data and it turns out that this data I got is the actual Pipi I data I got from, ah, couple of AP eyes I put together and I have the top 100 packages in all their details in a bunch of Jason files.<br>
So what we're gonna do is load those Jason files, pull them apart, do some type conversion and things like that and insert them all into a database that is super into D gritty and it's really not worth going into So let's just skim quickly across that first off to run the program we were going to use to new requirements progress bar to so we can have a cool progress as we're doing our import, which is really, really nice.<br>
And then Python Dash date util, which is a really, really nice way to parse dates much better than the built in stuff.<br>
So I've already pip installed these, so they're just in the requirements now.<br>
So over here at the top of your repository, I have the pie p I top 100 each one of these is just a Jason file.<br>
For example, let's look at click circa a little while ago written my r Monroe knicker originally at least now managed by David Lord and the Pallets Project.<br>
But for the day that we got, this is what it says And it has the licenses BST but notice it doesn't just say licenses BST it has this, like, sort of funky name space style, if you will.<br>
It talks about the languages Python and being Colin Colin three.<br>
We're gonna parts that apart.<br>
Yeah, the license BST.<br>
It works on Python to and Python three and so on so you can scroll through and see, like, here's all of our releases All the details about the releases and the dates and Wolf has a lot of stuff, right?<br>
So we're gonna go and parse that apart and insert it into the database and that's gonna happen over here pretty straightforward.<br>
What we're gonna do is we're going to just go and ask really quickly like, Hey, is there any data in this database and the way of checking is are there any users?<br>
It could look at all the tables and you just ask.<br>
Are there any users?<br>
If so, Hey, we've probably already done this, so don't reinsert duplicates.<br>
Just don't do anything.<br>
Do a little summary there at the end.<br>
But if it happens to be empty, go load up those files, all of them.<br>
All the Jason files skin across all of them, find the distinct users, import them, do all the packages and their releases and so on.<br>
Pull out the languages licenses like that colon, colon BSD license thing we just saw.<br>
And then finally do a little summary.<br>
So we're just gonna run this through its Let's just look at the import languages and goes and uses our progress bar, which is pretty, pretty sweet.<br>
And it rates over them and pulls out the language classification that the interesting data base part is it says that we're going Teoh, just create session creative programming language, set the details of it added to the session and call commit and then update our progress bar Super straight for right.<br>
This is what we did before.<br>
It's just all this gu of juggling the Jason Files.<br>
All right, so let's go and run this and because it uses the Progress bar It looks better outside apply charm.<br>
It will run in here.<br>
No problem.<br>
But let's just make it as nice as possible.<br>
So I want to figure out where to activate my virtual environment.<br>
That's a long enough directory, don't you think?<br>
I will say that.<br>
Slash Activate.<br>
And then I want to runs a Python.<br>
The name of this script here where that one is gonna do the import.<br>
One other thing we also need to add system dup half upend toe our path.<br>
This folder right here because we're importing pipeline, not or in pyjamas is gonna totally work smooth, because guess what?<br>
PyCharm does that forest right there.<br>
But if we try to run this outside without setting this up, it's a package or something like this is not gonna work so great.<br>
Now her ready to run our code here till gonna run Python out of our virtual environment Pointed at a low data Here goes.<br>
So hearing see, it's loading up all of the users.<br>
All the projects it found 96 packages said there were 100.<br>
I think for some reason, some couldn't be downloaded.<br>
So let's go with 96 Top 96.<br>
Out of there, it went through, and it found the users found the packages and releases the languages.<br>
So in the end we found 84 users.<br>
96 packages, 5400 releases embedded within those documents, 10 maintainers, 25 languages and 30 different licenses.<br>
All right, well, that's it.<br>
We should now have a whole lot more data over here.<br>
And if we go local quick, let's just go to the packages and jump to the console.<br>
Say, select star from packages.<br>
We run it like that.<br>
We had a whole bunch of them.<br>
Here's am Q P.<br>
Actors are pars, Flake eight and so on.<br>
We running for releases, you see a whole bunch of stuff and there related to their various packages over here on the right.<br>
Pretty awesome, huh?<br>
So now when we run our app, forgive over and run the actual app itself click on it, you can see Well, we're not quite using the data yet, but we're going to be able to start using all that data we've just loaded up and dropping it into these locations.<br>
So that's gonna be really awesome.<br>
We have true, accurate, realistic or even a snapshot in time.<br>
RealD data from Pipi I toe work with to finish building out in testing your app
|
|
show
|
2:22 |
One of the core concepts of SQLAlchemy is this Unit Of Work.<br>
Let's dig into that.<br>
The Unit Of Work is a design pattern that allows ORMs and other data-access frameworks to put a bunch of work together and then at the very end decide to go and interact with the database decide now we're going to actually save this data within a single transaction.<br>
So here we have a bunch of different tables customers, suppliers, and orders.<br>
They're all providing entities into this operation this unit of work.<br>
So maybe we have a couple of customers one supplier, and one order.<br>
We've also maybe allocated some new one like we did with package and we're inserting that into the database.<br>
And maybe we've changed things about the supplier we're updating those in the database.<br>
And the order is canceled so we're calling delete on that.<br>
All that gets kind of queued up in this unit of work.<br>
And then I'll call commit in SQLAlchemy syntax and that pushes all those changes which are tracked by the unit of work the so-called dirty records the things that need to be inserted the things that need to be deleted the things that need to be updated and so on.<br>
So we can use these unit of works like that and the way we create them are with these sessions.<br>
So we've seen that we create these engines and the engine gives us this session factory that was all encapsulated within our db_session class.<br>
We do this once and then every time we want to interact with the database we create one of these sessions.<br>
So we come over here and we call that session factory it gives us a unit of work which is often called a session kind of treat this like a transaction a bunch of stuff happens in memory then it's committed.<br>
Maybe we add something, maybe we do some more queries maybe that tells us what we've got to do to add some more.<br>
We could make changes to the results of those queries either updates or deletes.<br>
Other than the query there's not actually been any interaction with the database.<br>
This add doesn't actually add it to the database it just queues it up to be added.<br>
When you call commit that commits the entire unit of work.<br>
Don't want to commit it?<br>
Don't, then nothing will happen in the database there will be no changes to your data.<br>
And that's how the unit of work pattern appears and is used in SQLAlchemy through this session factory and then committing it.
|
|
show
|
5:55 |
Now that we've imported the data into our database I think it's time to start using it.<br>
How many projects did we import?<br>
Negative one, unlikely.<br>
That's not typically a count of real numbers, is it?<br>
So, what we need to do is actually go query the database to fill out those sections.<br>
We also need to fill out our releases down here at the bottom, but we're going to focus on just this little stat slice as we called it.<br>
So if we go over here, and we see right now what we're doing is we're going to our package_service and we're calling a function: get_latest_packages.<br>
Well, that's pretty cool, but we could look over here this is just fake data.<br>
So let's actually put in here we're going to need to pass more data over to our template.<br>
So we go to our template, it's super nicely organized.<br>
We're in Home View, so we go into the Home folder and run the Index function, so we go to Indexing.<br>
Boom, there it is.<br>
Well, that looks like a problem.<br>
Let's pass in some data.<br>
Now, we might want to just drop the number here like, package count, that'd be okay.<br>
Except for, if there's a million packages it's just 1-0-0-0-0-0-0 with no digit grouping.<br>
So we could do a little bit better if we did a little format statement, like this.<br>
Like so, and to do the digit grouping.<br>
Now let's just put that in for the others as well.<br>
I have release count, and user count.<br>
Of course, for that to work, if we try to run this again we go to refresh it, not so much.<br>
Unsupported format type.<br>
And let's go pass this data along here.<br>
So we're going to need to have a package count and let's just make this fake for a minute.<br>
Release.<br>
And user count.<br>
This should be auto-refreshing down here at the bottom.<br>
Here we go, it's active again, super.<br>
Refresh.<br>
One, two, three, looks beautiful.<br>
I was trying to pass None to format in digit grouping.<br>
None doesn't go that way.<br>
Right, so this is working in terms of the template but it's not working in terms of the data.<br>
So let's go and change this down here I'm going to call Package_service.get_package_count Now this doesn't exist, but it will in just a moment.<br>
Release count, we're going to do users in a separate service.<br>
So we can go over here and hit Alt + Enter create this function, and it didn't really know what to do.<br>
It's supposed to return an int.<br>
And over here, we saw how to do queries.<br>
If we start by creating a session and we're going to import our db so we'll say, import pypi.org.<br>
Get our little db_session there and we'll say create_session.<br>
That's cool.<br>
And then we simply have to do a query so we're going to come over here and we're going to say, I would like to go to the session and do a query, and then you say the type.<br>
We want a query package because that's what the count we're looking for.<br>
And we might do a filter to say where the package_id is this or the publish date is such and such or the author is this person or that but all we want to do is a super simple count.<br>
That's it, that's going to be our package count, and let's going to let it write that as well and we'll just grab this bit.<br>
So this one is the same except for instead of querying package, what are we querying?<br>
We query release.<br>
Clean that up, and then finally let's go over here and figure out where we are.<br>
We also want to have a user service.<br>
So if I come over here, and just copy paste I can change that to user.<br>
Do a little cleanup.<br>
All right, that looks pretty good.<br>
Now, it might seem silly to just have this one function but of course, we're going to log in, we're going to register there's all sorts of things this user session will be doing.<br>
So now if we go back we should be able to go to our import up here.<br>
And we'll do user service as user service.<br>
And here we'll do user_service.get_user_count.<br>
Okay, moment of truth.<br>
If we run this, it should be doing those queries from the database, creating our unit of work, our session.<br>
Now, it's pretty boring, all we do is do a quick query.<br>
We're not, like, inserting and updating data yet but it still should be doing some good stuff.<br>
Let's do a Save, which should rerun it.<br>
See the little green dot, it's still running so it should've rerun.<br>
Moment of truth: refresh.<br>
Bam, look at that!<br>
How cool is this?<br>
So 96 projects, 5,400 releases, and 84 users.<br>
That's exactly what we saw earlier.<br>
And if we go over to our little inspector and we go to the network, and we say just show us HTML and we do this again a couple of times you can see that the response time even going to the database and doing three queries is 11 milliseconds.<br>
That's pretty solid, right?<br>
Not too bad at all.<br>
So our site's working fast it's using the indexes that we set and it's pulling back the data.<br>
Well, it probably doesn't use an index for a count but it would if we were doing the right kind of queries like, for the new releases.<br>
So, I think our little stat slice is up and running and that's how we're getting started using this data that we inserted, that we got from those JSON files.<br>
Lots more of that to do really soon.
|
|
show
|
9:03 |
We have our little stat slice working just fine but the pieces here, not so much.<br>
Remember, this is just fake data.<br>
See the desk, all right, so now we're going to write the code to get the new releases.<br>
Let's go over here and have a look.<br>
So we're not going to call this test packages anymore we're actually just going to inline it Like that so we're going to go and implement this method right there.<br>
Obviously, it's not doing any database work yet, is it.<br>
Now, as we talk about this, there's going to be several ways to do this, some of them more efficient some of them less efficient and I want to look at the output, so the first thing I actually want to do is go over to our db_session and I told you about this echo parameter.<br>
Let's make it true and see what happens.<br>
You can see all the info SQLAlchemy looking at the tables to just see if it needs to create new ones.<br>
It doesn't and if I erase this and hit the homepage you can see it's doing three queries it's doing a count, against users against releases and against packages.<br>
Where is that, that is that part right there.<br>
Okay, now I just want to have it really clear what part is doing what, so I'm going to turn these off and just have them return 200 or whatever, some random number.<br>
I'm also going to do that for the user_service.<br>
So now let's refresh, I'll clean up this here and refresh, notice there's no database access.<br>
That's good because we're going to do database access here and we need to understand it really clearly.<br>
All right so let's go over here and work on implementing get_latest_packages.<br>
In fact, we were returning packages but what we really want to do is we want to return the latest releases with ways to access the associated packages.<br>
If we look at the release, remember each release has a package relationship here.<br>
So I'm going to change this around and rename this to latest releases like that and we're going to implement something here.<br>
It's going to start like this and let's go ahead and tell it that it is going to return a list of release, release is good, import that from typing.<br>
And it's also going to have a limb and that equals 10 or something like that.<br>
So if we don't specify how many latest releases we get 10.<br>
Okay so we're going to do some work and eventually we're going to return a list right here.<br>
Well what are we going to do, well it turns out that this is actually the most complicated thing we're going to do in the entire website.<br>
It's not super complicated but it's also not super simple so let's do one more import.<br>
I'm going to import sqlalchemy.orm and because we need to change or give some hints to how the ORM works.<br>
So let's go over here and say the releases are equal to, want to create a query so we've already done this a few times session query of release, now what we want to do is get the latest ones so we're going to do an order by.<br>
So we'll come down here and say .order_by and when we do an order by we go to the type and it's the thing we want to order by and we can either order ascending like this or we can say descending like so.<br>
That's a good part.<br>
And maybe we want to only limit this to however many they pass in, 10 by default but more, right, that's the variable passed in that's the function it takes and we want to return this as a list.<br>
So snapshot this into a list we're going to just return releases.<br>
So is this going to work, first of all, is this going to work?<br>
I think it will probably work.<br>
I'm certain it will be inefficient.<br>
Let's find out.<br>
So we rerun this and just clean that up here and let's go hit this page remember these are all turned off so the only database access is going to be to derive a little bit, if I refresh it what happens?<br>
Sort of works, well it actually worked let's go fix it really quick and then we'll come back and talk about whether it's good.<br>
So here we actually had r in releases and I think we also need to pass that in from our view 'cause even though we called this releases this packages so let's make it a little bit more consistent, releases.<br>
That's not super, p is undefined okay and now we can fix this.<br>
So we have r, now remember r is a package and it doesn't have a name but it has an ID and then r, it doesn't have a version it has a major, minor and build version but it also has a property which we wrote called version text.<br>
So if we go check out version text it just makes a nice little formatted bit out of the numbers and that make up it's version.<br>
And then here want to go to the r and we're going to navigate to its package and then we're going to say summary.<br>
Let's see what we get.<br>
Ooh, is it good, it is good, it's super good.<br>
We were actually able to use that relationship that we built to go from a bunch of new releases back over to a bunch of packages.<br>
Now there is some possible issues here.<br>
We could have these show up twice.<br>
AWSCLI two versions, but maybe that's okay.<br>
We're showing the releases not just the packages.<br>
However if I pull this back up, ooh, problems.<br>
Look at all this database access.<br>
Let's do one clean request.<br>
So here we are going in to our releases and then we go to packages and packages and packages over and over again.<br>
Why is that happening?<br>
It's happening every time we touch it right here, that's a new database query.<br>
Moreover that database query is actually happening in our template, which is not necessarily wrong but to me, strikes me as really the inappropriate place.<br>
In my mind I call this function database stuff happens and by the time we leave it's done.<br>
Well, let's make that super explicit.<br>
Let's close the session here and see how it works now.<br>
It's going to work any better?<br>
Think that's not better, not so much.<br>
It's a detached instance error.<br>
The parent release has become unbound so we can't navigate its package property.<br>
Actually we don't want to do that.<br>
It's okay that we close it here, we don't want more database access happening lazily throughout our application.<br>
Instead what we want to have happen is we want to make sure all the access happens here.<br>
So we can do one cool thing, we come over here and say options, so we're going to pass to this query we can say SQLAlchemy.orm.joinedload and I can go to the release and I say I would like you to pull in that package.<br>
So any time you get a particular release also go ahead and query join against its package so that thing is pre-populated all in one database query.<br>
So by the time we're done running it we no longer need to traverse reconnect to the database for this.<br>
Is it going to work, well let's find out.<br>
Hey it works, that's a super good sign.<br>
All of the data is there and look at this.<br>
Let's do one fresh one so it's super obvious.<br>
That's it, so we come in and we begin talking to the database, we do our select releases left outer join to packages and where it's set to be the various IDs and whatnot and a limit and offset and then this rollback here is actually what's happening on line 18.<br>
So we're just, we started a transaction when we interacted with it and it says okay actually we don't need this anymore, roll it back.<br>
Which is fine you've got to close your transaction either commit or rollback so rollback I guess.<br>
We don't really try to commit anything and we didn't want to so this is good.<br>
How cool is that, huh?<br>
I think this is pretty awesome.<br>
So we've done a single database query we've solved the N plus one problem and we've got our latest releases and we used the latest releases to pull back the package names and the package summaries and so on.<br>
So that we know our database stuff is working efficiently let's go and put these queries back here.<br>
So it's working for real.<br>
Go back and pull up our inspect element.<br>
Remember we're still running the debugger but we should get some sense of performance here.<br>
There we go, 13 milliseconds.<br>
What was it, 11 before, so going and get those releases and those packages in that join barely added any effort.<br>
Now remember we're running the debugger we could probably make this faster still but this homepage is working super super well.<br>
I'm really happy with how everything's coming together.<br>
And if we have true indexes it doesn't matter if we have a decent amount of data our queries should still be quite quick.<br>
All right, awesome, homepage is done.
|
|
show
|
9:18 |
The last demo driven part we're actually going to write during this chapter is what happens when you click here.<br>
Right now, it's a little underwhelming.<br>
So notice, we don't even have a link.<br>
But if I were to, you know, hack it in there project/flask or something like that it just says details for flask.<br>
Let's compare that against the real one.<br>
Huh, well, those are not quite the same, are they?<br>
I mean, we've done pretty well on the homepage here going back and forth but certainly not on the details.<br>
And if we click it, it doesn't even go somewhere.<br>
So that's what we're going to focus on in this lecture here.<br>
Well, if we go to the homepage, the index this is easy to change.<br>
This is going to be /project/{{ r.package_id }} Let's just get that working now.<br>
I can click it and it at least takes us to where we intend it to go.<br>
Now, it turns out the HTML here is quite complicated.<br>
We've spent a lot of time already putting the design in there and this is just a variation on that design.<br>
We have the heroes, we have Bootstrap all the kind of stuff.<br>
So I'm not going to go over it again.<br>
I'm just going to copy some over and we can just review it really quickly.<br>
We're going to focus on getting the data back and delivering it to that template.<br>
So, towards that, let's start over here on this page.<br>
And right now we just say package details for some package name, but what we really want is we want to get the package from the package service.<br>
And we'll do something like get_package_by_id or pass package_name.strip().)lower().<br>
Okay, well, that's not so incredible.<br>
I mean, maybe we even want to do a little check here.<br>
So if not package name just so that doesn't crash we might do a return flask.abort.<br>
Status equals 404.<br>
Something like that.<br>
Okay, but let's assume this is working.<br>
And down here now.<br>
And we can go to a query.<br>
It still might not be found.<br>
So we might say, if not package, again, not found.<br>
Right?<br>
They could be they asked for a package, abc one, two, three, it doesn't exist.<br>
So we don't let that to crash.<br>
Now, let's go and have PyCharm write this function.<br>
It's going to be package_id, which is going to be a str.<br>
It's going to turn in optional package.<br>
We're going to import that from typing.<br>
It's optional 'cause, like I said you could ask for a package that doesn't exist in the database.<br>
Super.<br>
Well, how's it start?<br>
How do all of them start?<br>
They all start like this.<br>
And somewhere we say db_session.close probably.<br>
And in the middle, we do things like get us the package.<br>
And the package is going to be, again, one of these sessions.<br>
We're going to create a query a lowercase query of package filter and what we want to do is say package.id == package_id.<br>
And again, maybe this one would want to do that test.<br>
Actually something like this.<br>
It's a bit of a duplication but let's go package_id.<br>
Instead of returning abort, we'll return none which will see it as missing then it will abort it.<br>
And then here we'll just say package_id equals package_id.<br>
That's true.<br>
Okay.<br>
It's a little bit of data protection because that is user input right there.<br>
All right, so we're going to get our package.<br>
Now, if we do it like this, we get a query set back.<br>
Not what we wanted.<br>
All right, we don't a want a query set.<br>
What we want is, we actually want one package.<br>
So we'll go over here and we'll say first.<br>
And that's either going to return the package or nothing hence, the optional right there.<br>
Then we return package, like so.<br>
And we should be good.<br>
We can just do a really quick test here.<br>
And let's just do package.<br>
So this will show that we're either going to get a 404 or we'll get it back and will show its name when we click on the homepage there.<br>
Let's try.<br>
Put down, click on boto.<br>
Details for boto, woo!<br>
We got that from the database.<br>
It's pretty cool, right?<br>
And so super easy.<br>
I mean, seriously, like that is easy.<br>
However, our little package details page actually needs more information than what we have here.<br>
So we go look through this.<br>
You can see we're adding a new style sheet to the top of the page.<br>
And we're having our hero section, it has a id and package this and package that but it also has a handful of other things.<br>
So it wants to work with the releases.<br>
Now this is going to cause a tiny issue that we're going to catch and then improve.<br>
So we're going to have latest version is going to be zero.zero.zero to start, okay?<br>
We're also going to have latest release is going to be None.<br>
And we'll have to say is latest equals true.<br>
So the page adapts on whether or not you have the latest version.<br>
We're just going to say it is for now.<br>
We need to actually have the instance that is the latest release, if it exists and also the text of the version.<br>
So we'll say this, if package.releases remember this is sorted in the order of newest first.<br>
So we can say, latest release is package.releases of zero and latest version is going to be latest release version text.<br>
And we'll just leave is latest is true.<br>
Now the other thing we want to do instead of just returning the string is we want to go over here and say, remember to this?<br>
And we said, response, this was from long ago.<br>
And what was it?<br>
Template file is going to be it's going to be where is it in this folder?<br>
It's going to be packages slash details.<br>
So packages slash details.html.<br>
And then for this part, we don't return that.<br>
We return a dictionary of stuff.<br>
And there's a bunch of stuff that I got to put I here.<br>
So I'm just going to copy this over.<br>
There.<br>
So we have our package, we want to hand that off.<br>
Latest version, latest release whether or not it's the latest.<br>
Let's make this a little bit more obvious.<br>
Pass it through.<br>
And of course, a lot of this could be computed in the template.<br>
That would be wrong.<br>
The template is about taking data and turn it to HTML and not doing all this logic.<br>
It's best to get this as much ready for the view as possible.<br>
Here, we're going to talk about patterns that make this better later but right now it's already pretty good.<br>
Let's just rerun it to make sure we're all good.<br>
Over here and refresh.<br>
Now, what happened?<br>
Why did this crash?<br>
Well, if I didn't do that close inside here my package service, it wouldn't have crashed.<br>
So you might say, Well, just don't do that.<br>
Again, this means that like database query operations and lazy loading is leaking into our template.<br>
So one option is, we'll let it leak and everything will work.<br>
The other one is, well, what do we do up here, we add in one of these joined loads.<br>
We kind of basically need the same thing but in reverse.<br>
Let's put the dot here.<br>
And not there.<br>
So instead of saying I want to go to the release and load the package I want to go to the package and load the releases, plural.<br>
It should've reloaded when I saved, and ta-da.<br>
How cool is that?<br>
Look, it's already working.<br>
Yes, I copied over the HTML and the CSS.<br>
But that's not the hard part, is it?<br>
I mean, maybe it's hard at some point but that's not really the essence of this data-driven app.<br>
So here we've got our pip install boto.<br>
Here's the version.<br>
Is it the latest version?<br>
Yes, that's why it's green.<br>
You can go, here's a project description the release history.<br>
If I want to go to the homepage click on that, it takes me to the Boto3 homepage.<br>
Right?<br>
All this stuff.<br>
Let's see what we get if we put Flask up here.<br>
We have Flask, and we have all sorts of stuff like here's the license.<br>
We go to the homepage, we go to Pallets, and so on.<br>
And we don't have every bit of details in here that the main one does, but good enough for our demo app, right?<br>
This is cool, huh?<br>
We did some query against our database filtered by the ID, got the first one but then realized, oh, we're trying to navigate that relationship.<br>
So instead of doing the in plus one interactions with the database and, you know, leaking and lazy loading let's explicitly catch that by closing it.<br>
And then do a joined load to eager load it all in one query.<br>
That's pretty awesome.<br>
So now things are going really, really well.<br>
There might be times when you don't always want to do this and you might have to have a little flag you pass on whether or not you do this just because if you don't want it it's a little bit extra overhead on the database but generally, given a package, we want its releases.<br>
So I'm pretty happy to just put this right into the main method.<br>
All right?<br>
That's it.<br>
We have our data-driven package page and our homepage up and running.<br>
I'm really happy with the way the site is coming along.
|
|
show
|
4:03 |
We've written a few interesting queries and before we're done with this application we'll write a couple more.<br>
But let's talk about some of the core concepts around querying data.<br>
So here's a simple function that says find an account by login.<br>
We haven't written this one yet but, you know, we're going to when we get to the user side of things.<br>
It starts like all interaction with SQLAlchemy.<br>
We create a unit of work by creating a session.<br>
Here in the slides we have a slightly different factory method that we've written, but same idea.<br>
We get a session back, we're calling it s.<br>
We go to our session and we say s.query of the type we're trying to query from account, and then we can have one or more filter statements.<br>
Here we're doing two filter statements.<br>
Find where the account has this email and the hashed password is the one that we've created for them by rehashing it.<br>
And now we're calling one, which gives us one and exactly one, or None, items back and we're going to return that account.<br>
So if you actually look at what goes over to the database it's something like this: select * from account where account.email is some parameter and account.password_hash is some other parameter and the parameters are: Mike C.<br>
Kennedy, and abc.<br>
You'll see that you can layer on these filter statements, even conditionally.<br>
Like, you can create the query and then say if some other value is there, then also append or apply another filter operation so you can kind of build these up.<br>
They don't actually execute until you do like, a one operation, or you loop over them or you do a first, or anything like that.<br>
So here's returning a single record.<br>
Also, it's worth noting that the select * here is a simplification.<br>
Everything is explicitly called out in SQLAlchemy.<br>
The concept is, just give me all the records or give me all the columns.<br>
If we want to get a set of items back like, show me all of the packages that a particular person with their email has authored we would go and again get our session we would go and create a query based on package we would say filter, package.author_email equals this email.<br>
==, remember, double equal.<br>
And then we can just say, all.<br>
And that'll give us all of the packages that match that query.<br>
This one's not going against a primary key so there'll be potentially more than one.<br>
Of course, this maps down to select * from packages, where package.author email equals well, you know, the email that you passed.<br>
Super simple, and exactly like you would expect.<br>
So the double equal filter, pretty straightforward.<br>
There's actually some that are not so straightforward.<br>
So equals, obviously ==.<br>
user.name == Ed, simple.<br>
If you want not equals, just use the != operator.<br>
That's pretty simple.<br>
You can also use Like.<br>
So one of the things that takes some getting used to is these SQLAlchemy descriptor column field the how you type multipurpose things here is they actually have operations that you can do on them when you're treating the static type interacting with the static definition rather than a record from the database.<br>
So here we say, the user type.name.like or N or things like that, and so there's, you know we saw the descending sort operation on there as well.<br>
So if we want to do the Like query, this is like find the substring Ed in the name, then you can do .like and then pass the percent to operators as you would in a normal SQL query.<br>
If you want to say, I want to find the user whose name is contained in the set Ed, Wendy or Jack, then you can do this .in_ remember the underscore is because in is a keyword in Python, so in_.<br>
If you want to do not, not in, this kind of a not obvious but you do the Tilda operator at the beginning to negate it.<br>
If you want to check for Null, == None the And you just apply multiple queries.<br>
The Or doesn't work that way, if you want to do an Or you've got to apply a special Or operator to a tuple of things.<br>
So here are most of the SQL operators in terms of SQLAlchemy.<br>
You can do a lot of stuff with this.<br>
It's not all of them, but many of them.
|
|
show
|
0:44 |
Databases are really good at filtering and ordering.<br>
Here is a function, find_all_packages and the idea is I would like, ideally a list of all the packages in the database showing the newest ones first and the oldest ones last.<br>
So we're going to do a query on package and we don't do any filtering because like we said the in name, we want them all.<br>
But we are going do an order_by so we say, query of package.order_by and then we always express these operations in terms of the type, so package.created If we just said package.created it would order ascending by the created_date but we want descending, so we go into that descriptor created and we say .desc we call the function to reverse the sort, and then we just say give us all the packages, here they come.
|
|
show
|
0:49 |
In order to update existing data we start like we always do, we get a session.<br>
And then we retrieve one or more records form the database.<br>
Here we're just getting one package.<br>
When I get this package back, and if we make changes to it so we set the author value to a new name we set the author email to a new email SQLAlchemy will track within the session that that record is dirty and it needs to be updated because we've changed some fields.<br>
And then, when we're ready to actually save it push all the changes back.<br>
I could apply this to as many entities as we'd like it doesn't have to just be one.<br>
Then we're going to commit the unit of work and it's going to look at the changes do all the changes in a single transaction back to the database.<br>
We use the unit of work to do our updates just like we do the inserts.
|
|
show
|
1:59 |
We saw relationships are extremely powerful and let us imagine as if our code and data were just connected, almost hierarchically the way we would in a regular Python program connected not split apart like we do in a database.<br>
The way we define these we add an orm.relationship field to the class so here we have releases so the relationship is against the release type that we're speaking in terms of SQLAlchemy entities, not database and then we're going to do an order_by this can either be a single thing or a list.<br>
Probably an iterable actually.<br>
And so, we're going to pass that in and then we're going to say it back populates package.<br>
What does that mean?<br>
We want this relationship to work in both directions so if we have a package we can see the releases and if we have an individual release.<br>
We can see the single package that it corresponds to.<br>
So, over in the release we're going to do the package_id value to actually store the relationship like we would store any value that's a string or an integer whatever it corresponds to in that other field.<br>
And then we're going to say this is a foreign key and in the foreign key part we speak in terms of database, packages.id.<br>
But them we also would like to establish that relationship so we say there's ORM relationship for the package type from release back to the package and it back populates releases and it's called Package so you can see the symmetry here not too hard to set these up once you have an example.<br>
Put them side by side you go okay, here's where I fill in the pieces for my particular dataset and then you saw that when we load a package it doesn't actually load the releases unless we interact with it.<br>
So, if we touch it it'll go back to the database and do the query.<br>
We also saw that if we create new releases and put them into the release package.releases well, it becomes a list and commit those changes that'll actually insert the releases.<br>
We work in the same but in the reverse as well if we had set the package on a release so it's sort of bidirectional in that sense.
|
|
show
|
0:37 |
We started off this chapter by demoing how to insert data.<br>
Let's actually summarize now here at the end.<br>
So again, we're going to create a session which is the corresponding unit of work bit of syntax in SQLAlchemy.<br>
Then we're just going to allocate some objects so here we're going to create a package and set its ID and its author maybe create some releases set the release, not package value other properties similarly relates to set its package, we're going to add the package call commit, whoosh!<br>
All three that are seeing this because of the relationships get inserted into the database; super easy!
|