 Kia ora, everyone. Hello. This is pretty exciting. Actually, the last time I presented to this many people, that was at Open Mike Night of the Comedy Club. That doesn't bode well. Thank you. That's more than I got back there. OK. So thanks, guys. My name is Phoenix. I've been developing software for about 10 years now, most recently in Python. I've also worked with Java, PHP, Dflat, a little bit of C. I currently work as a senior software architect at the company called Lendo EFL. We are a psychometric credit scoring company based out of Peru in the Philippines. We work with partner banks all over the world to help develop credit scores for applicants, entrepreneurs, value creators in countries where the credit infrastructure is not great, or they just don't have any credit history. So we found ways to generate credit scores using alternative sources of data. And then on the weekends, I work on the Python library for the IO to cryptocurrency. So that's me. Thank you for bearing with me. Let's get that out of the way. So this is demystifying setup tools, entry points, or, and Tommy, thank you for not stealing my thunder and for that. OK. So before we dive in, just real quick, show of hands. How many of you are already familiar with setup tools? You've written setup.py or maintained. OK, cool. So it's about half. Cool. So I think what I'll do, that wasn't just for my curiosity. I'll go a little bit slow at the beginning, just for those of you who are not so familiar. And then for those of you who did raise your hands, just correct me if I get anything wrong. So entry points. So entry points give you a way of creating a global registry of Python objects. So those could be classes, functions, methods, modules, packages. Doesn't matter. If you can construct a dotted path to it, then you can put it in entry points. And then you can assign a group name and an identifier to it. Now when I say global, I don't just mean global to your package. I mean global to your entire virtual environment. So entry points that are defined in one package are accessible to all the other packages and so on. And we'll see why that becomes important a little bit later on. But before I dive into all that, we're going to do a very simple use case. This is something that, to be honest, probably most of you have already done. You just maybe didn't realize it at the time. So why not another quick show of hands. How many of you are familiar? You've created a Django project. Awesome. OK, cool. So Django, I'm sure we all know, is a web application framework. So when you're getting started, in fact, let me see if I can do that right now. It's always a bad move to start anything during a presentation with let me see if. But all right, let's create a temporary virtual environment just so you know I'm not cheating. And we'll do Python 3 today. And we'll let that go. And hopefully this won't take too long because it should be cached locally. I've rehearsed this thing for the last 48 hours. In fact, I've not gotten any sleep. So this should go smoothly. All right, you've got five seconds. OK, good enough. It heard me. It's amazing. All right, install Django. So the first thing you do is you do an install. And again, hopefully this will take about five seconds. Good. And then the next thing you do is you wait. There we go. All right. And we type Django admin start projects. Let's do demo. It doesn't really matter. And boom. And so then here's my demo. But hang on just a second. Where did this guy come from? Like, I mean, yeah, obviously I did a pip install Django. So something put it on my path. But if I look locally, I mean, there's no Django admin anywhere in my local directory. So clearly something happened there. So what I want you to do is I want you to keep that in the back of your mind, fold it up, put it in your back pocket. And we'll look at it later. The example, not your back pocket. Never mind. This is why I don't improvise. OK. So what I'm going to do instead, now that I've kind of teased you with that, is I'm actually going to show you how to do that with another application. So as I mentioned in the beginning, I do work for a credit scoring company. So one of the tools we have is our scoring engine, where we collect data from a credit application, and then we turn that into a credit score of some kind. Now the scoring engine is called Skynet. I actually don't know why it's called Skynet, and all the people that might know have moved on to other companies. So I'll probably never find out. I can tell you it's not self-aware. I'm pretty sure. But the code coverage is only about 30%, so no, no, no. So what I'm going to do is I'm going to drop here into our little app here called Skynet. Ignore these guys. We'll come back to them in a little bit. So yes. Now obviously, this is like Don Kirk or your average found footage movie. It's inspired by real events. This is not our Skynet application. I wrote this last week for this demo. But it's based on something very real. So I've got a main function right here. And the idea is that I want this to be the thing that we run to make Skynet work. So when I type Skynet on the command line, I want this function to run. And it's going to load some variables from argv, and it's going to figure out which command I wanted to run, kind of in the same way that Git does. You type Git add, so it knows that you're running the add command, things like that. Keep in mind, the demo is just don't take the demo too seriously. Just think about the concept that we're going to prove here. So again, if this were not using entry points, we would do something like this. We would do if name equal equal to main. I'm not even going to let it run. You know what I'm getting at here. And do main, right? And then maybe what you can do then is you can, so now you can call, in this case, what is that app? Why am I looking up there? I can see it right here. You can call app.py. I do this a lot, can you tell? So you run app.py. But then, if we want to type Skynet, we have to set up an alias, or maybe a symlink, right? But that's all manual. And I have to do that for every single installation. Can you imagine if Django worked that way? If you did a pip install Django, and then you had to copy some script into your bin directory? No. That's ridiculous. We're going to make entry points do the work for us. So with that, let's dive into our project's setup.py. So for those of you not familiar, and that's roughly half of you, unless you're being dishonest or modest. As you can see, there's not a lot here. If we forget about entry points for just a second, this is what? 14 lines, including some white space. And I'm being generous. So what do you do? So from setup tools, import setup. And we also have find packages, which I'll explain in a second. Shame on me. I didn't need it for this project, but we'll do it anyway. It has a name. The name is important just because when you install a package, setup tools is going to install a little bit of metadata. And it needs a directory that it can put all that metadata into. I'm simplifying a bit, but you can think of that as your project's egg. That name should be unique, because if you try to install two packages with the same name, you're going to run into problems. But other than that, it can be pretty much whatever you'd like. Version, I actually don't even think that's necessary. But it is nice, and it makes things feel official, so why not? Packages. So one of the things you're going to do a lot when you start working with setup tools is you're actually going to create distributions. That could be like a source distribution, which is just a tar ball of your code. Or it could be what's called a wheel, which is almost exactly the same thing, just with a couple of other things sort of pre-compiled, effectively. The catch is, though, that when you're creating these distributions, you can think of it like, if I want to distribute the Skynet application to the production environment, maybe I don't need to include the unit tests, or maybe I have some scripts that I run locally just for development. Those don't need to be distributed to production. And so this packages directive just tells setup tools that in this project, these are the code files. So in this case, and it's called packages, so we have a Skynet package, and this is where the main code of the application lives. And your project may have a few of them. I'm using find packages because I use it out of habit very, very briefly. And if you want to know more, you can see me afterward, and I can show you. But packages is not recursive. So if you had a really deep package structure, you'd have to start listing it all out. Find packages basically does that recursively. So that's why I use it. And then the last thing, before we get on to what you're all here for, is install requires. So I won't even do a show of hands, because I'm pretty sure all of you know this. I'm sure you've all interacted with it, a requirements.txt file before. This is basically the exact same thing. It just provides a slightly alternate syntax and a few other options. In fact, there's even a way that you can declare your project's requirements in setup.py and keeperrequirements.txt and keep them in sync without having to touch one or the other. Again, that's something you can see me after if you're curious about. For now, just know that Skynet requires the six library. Six is a library that you can use to write code that just works in Python 2 and Python 3. You don't need to use two to three or anything along those lines. Again, based on the true story, we migrated from two to three. So we started getting into that. OK, intro over. Let's get to the meet. Entry points. So again, if you can remember, because I know it was 20 minutes ago, sorry, the goal here is to install a Skynet executable into our path. And we're going to do that with entry points. So another directive that we add to our setup function is entry points. It takes a mapping. The keys are what are known as groups. So we have a group here called console scripts. Now, for the moment, let's just say it's magical. Actually, that's not much of a stretch. It is magical. But the point is it has to be called console underscore scripts all lowercase. Now, once you've defined your group, you give it a list, and inside are your entry points. So you can have as many as you want. You can't do it that way. Well, you could do it that way. You can have as many as you want. But the idea is, for each application that we want to install, we're going to add a separate entry point. So in this case, I just want the Skynet application. So I'm going to have just the one. Each entry point has a name. So in this case, because I'm really going for a motif here, we'll call it Skynet. So this is the thing that I type on the command line. So if I called this Skynet-main, then when I run this and install it, I will type Skynet-main to run my program. In this case, I'm just going to make it Skynet. And then there's an equal sign. And then finally, there's a reference to something. So this thing has to be callable for reasons I'll get to in a second. But other than that, this can be whatever you want. So we have a dotted path here to a module. So Skynet.app, so that's this guy here, Skynet.app. And then a colon, so to distinguish the module from the thing inside the module. And in this case, it's called main. So in Skynet.app, we have this function called main. So this entry point, if I did this all correctly, and I sure hope so, because I tested it 48 times in the last 48 hours, when I do a pip install, this should put a Skynet executable on my path so that I can type Skynet and run my Skynet.app. And again, to make sure I'm not cheating, I'm over here in this virtual environment. So the only thing in here, aside from some residual utilities, is just jangling and pits, pits, pites. I don't know. It doesn't matter with my accent. I'm mispronouncing it anyway. So we're going to do a pip install. And I'm going to do a dashy. I'll come back to that for those of you that don't know. So over here, this is where I'm keeping all my source files. So in documents, deceptemo. So I'm going to install documents, deceptemo. And I'm going to install Skynet. So again, for roughly half of you, when you do a pip install, all pip is really looking for is a directory that has a setup.py in it. So you may be used to doing a pip install and then like a pipy package. That works because it downloads the package from pipy, unzips it, and there's a directory with a setup.py in it. Similarly, you can just install something locally just by providing the path to that directory. Pip will find that setup.py and install it from there. Now the dashy, hyphen e, stroke e? Oh, that sounds terrible. That thing, or rather wherever it is, what that means is it stands for editable. So what I'm doing is I've created this brand new virtual environment somewhere in my file system. It's totally separate from where my source files are. Now when I do a pip install, pip is going to take all my source files and copy them into my virtual environment. But over the course of this demo, I'm going to be changing some stuff in the Python files. And I don't want to have to keep doing a pip install. I will have to for certain things, but when I'm making code changes, I just want them to constantly be updated. So what the dash e does is it installs a sim link to my project as opposed to copying everything over. So again, once you start getting into working with setup tools, you will end up using this a lot. So let's do this. Enough talk. Let's see if it works. Ooh, I'm looking down and my voice gets deeper. I love this. I might do the whole rest. No, I'm not going to do the rest of the talk this way. OK, Skynet. All right. And there we go. We'll get into what all that does later. But basically, it worked. So if we go back to, let's pull out that Django example, which is probably soggy by this point, sorry. Oh, I really should not improvise. It's the comedy club all over again, I swear. Let's look at Django. So here I'm over on GitHub. This is Django setup.py. Here's our entry points, console scripts, Django-admin. And here's the reference to the function that runs when you type Django-admin. And just for fun, here's piota. This is the Python library for the iota cryptocurrency. Again, here's our entry points, console underscore scripts. The command is called iota-cli. And this is the reference to the function. And just for fun, here's comjs. And we go over to setup.py. And there's a whole bunch here. We'll get into that. In fact, oh, you may actually know more about entry points than I do, actually. Come to think of it. But we'll focus on these guys. Here's our console script. So if you install comjs, you can type comjs, and it will execute this function. Everyone likes to call it main, don't they? Well, I wrote this one, so that one doesn't count. But you have to call it main, too. OK, anyway. Now, just of note, someone's raising their hands. That wasn't the note I was going for, but let's, what the heck, let's do it real quick. Just to clarify, with a console script's entry point, that keyword, that means when you type skynet on the command line, you'll get the main function to run. But it won't be the case that if you're in a Python environment, you import skynet, and then you type skynet, it's not going to run. Correct. Correct, yes. And the reason why that works, so, yes, so that, well, you got the, I'm so used to repeating the question, but you got the microphone, let's just go with that. Everyone heard the question, right? OK, I see some nods and thumbs up. That means OK in this part of the world. Good. So if I actually, if I do a witch skynet, I see this guy right here, and if I open him up. So this is actually what Setup Tools did, is it created this file, and it put it on my path. So that's why Skynet works, is because whatever shell you're using is looking for this specific file when you type that in. And in fact, ooh, cool, hey, that's a spoiler. We'll come back to that, all right. There, OK, I sure showed you. OK, so that's cool. So still some questions, because if that was all you could do with entry points, then this would be kind of over-engineered. I mean, then why do you need the magic console scripts and so on and so on and so on. So what we're going to do, we're going to go into a second use case. So you kind of saw that when I run Skynet, I get this menu of command. So we have score, which would be to take the data from a credit application and generate a credit score. We also have something called validate, which you would use to test the stability of modeling features. And so you can probably already see, we actually do have a registry here, where we're assigning some kind of identifier to a Python object. So if I were to go back into the Skynet code, and I'm hoping you already kind of know what this looks like, we have this registry. We'll call it registered commands. And we just add some. So we have here like a name. So here's help and then some kind of a reference to a Python object, help commands. This is some class. Obviously, I've just stubbed them out. We don't need to see how Skynet works. That works on multiple levels. So hopefully that looks kind of familiar, because we just did something just like it. We created a set of mappings. We gave something a name and then a reference to a Python object. So we're going to take this, and we're going to pretend because, well, we're not going to. I can say whatever I want, can't I? So we're going to pretend that Skynet is now some open source project where it's widely used. And some people are really loving it. We get a lot of users. They love this functionality. But then there's a smaller group of users that need some new feature. They need some new command. So you're knocking your head already. You know where I'm going with this. So they've built their own package, like a plugin almost. And they've built some batch commands. So let's say we've got a smaller group of users that do a lot of volume. And they don't want to be doing this one at a time. What they want is they want to run one command and split their workload up into batches, maybe like 1,000 at a time. The catch is, though, this is only for a small part of your user base. So you don't necessarily want to start including all these third-party commands in your base application. What you want is to create a pluggable interface. Now, to be clear, there are a lot of ways to do this. In fact, even this approach here will work. You just have to write some boilerplate code to sort of modify it on the fly. But we're going to do this with entry points, because, well, by golly, that's the name of the presentation. And honestly, and depending on your use case, it actually could be easier to do it this way. So I'm going to do a little bit of, I'm going to check out step number two. Very good. And we're going to go into R. So pretend that Skynet batch. And by the way, I apologize. If you're not familiar with Setup Tools also, I know it's a little confusing. I have a directory called Skynet and another package called Skynet inside of it. When you start getting into package distribution with Setup Tools, this is actually a very common convention, is you'll have one directory that represents your project, and then you'll have your main package inside of it. And they tend to be named the same. Not always. It's just a convention. So I apologize, though. I could have broken convention for this talk, such as life as CS and so on. OK, so here I've created two new commands in this other package. I have my batch score command and my batch validate command. This is a separate project. I've just called it Skynet batch because I'm uncreative. And the goal is I want to take these batch score and batch validate commands, and I want them to show up in here in registered commands. But I don't want to have to change any code in my main Skynet application to make that happen. I mean, I'm going to for this part. But then think about, in the future, when we add more and more and more plugins, it should just be automatic. So the way we're going to go about this is, that's a spoiler, I'm going to go over here into setup.py for Skynet batch. So again, this should look relatively unsurprising, name, version, packages, install, requires. Here's our entry points. But notice here that I'm using Skynet.commands instead of console underscore scripts. Now, as I said, this is a little bit magical. This can be whatever you want. The only guideline I would recommend is you'll often see it some kind of a prefix as a convention. The reason is that entry points is a global registry across your entire virtual environment. So when multiple packages declare the same entry points with the same group name, they all get merged together. And that's really good, because that's actually what we want, which you'll see in a second. The trick is, though, if you name it two generically, so if we just called this commands, and then we installed some other package that also defined some commands entry points, you're going to get this conflict. I mean, it'll still work, but the problem is, is now you're kind of polluting your name space. You're injecting some classes, in this case, totally unrelated to Skynet into your entry points. So to work around this, we just kind of use a prefix as sort of a very crude name spacing technique. Once you've defined your group, though, everything is exactly the same. So we have some kind of unidentified or a name, so I'm doing batch-score, batch-validate. You put an equal sign, and then you have a path to something. So in this case, we don't want to use functions. We want to use classes. So I got skynet underscore batch.commands, that's this guy right here, colon batch-score-command, or batch-validate-command. Again, that's these guys right here. Now just to be consistent, I'm going to go back to, not you, I'm going to go back to my base, skynet app, and so our console scripts are still hanging out because I still want skynet to live on the command line, but I've then added my own skynet.commands. So I've replaced that mapping that I put explicitly in the Python code, and I've translated it into entry points. Now, so far so good, but this still isn't going to work because all I've done is I've created some entry points. It's like putting records in a database. If you don't write any code that's looking for those entry points, then they don't do anything. So to do it, to accomplish this, we're going to use a library called package resources. So another quick show of hands. How many of you are familiar with the inspect library built into Python? Okay, not bad. That's a little, that's about a third because that's important. So for the rest of you, so inspect, okay, actually here's another one. How many of you are familiar with reflection? Just everyone who knows about inspect. Okay, fine. So inspect is reflection's library. Reflection is where you use the code to inspect the code itself. So you'd use reflection to see, you take given a function, what file is it defined in? And even to extract the code where that function is defined. Or given a class method, what are the arguments named? And things like that. So stuff we're like looking at the code itself to get information about the code. There's another package that comes with setup tools that's called package resources. Package resources is like reflection for your entire virtual environment. So you're not just inspecting classes or functions or things like that, you're actually inspecting things like what packages are installed, what versions of those packages are installed, what are their dependencies, things like that. And one of the things that you get access to through package resources is entry points. So I'm going to break convention real quick. I'm gonna do a quick experiment. We're gonna drop back into our temporary virtual environment. I'm gonna do another pip install. Let's get skynet. And I'm also gonna install skynet batch. Oops. Doop-a-doop-a-doop. Okay. And then I'm gonna drop into a Python shell real quick. So if we from package form, oh, this is not going well. Resources import, hitter, entry points. All right. And then I'm just gonna do a list. So itter, entry points. And we're gonna use our group name. So in this case, we picked skynet.commands. Let's get that guy in there. And let's just take a look. And I'm gonna do, let's just start with that. Okay. Oh, that was, well, okay. Fair enough. All right. See, this is why I don't do live coding. And we're just going to print, we're gonna do a wrapper, okay. So there is our five entry points, right? So we now can access those entry points that we declared from our code. We still have to get them to do something, right? But at least now we know that they're accessible. So if I go back, let's go back into skynet. I've changed this a little bit when you weren't looking. And we're gonna start off with registered commands. You can do this with a disk comprehension. I'm gonna use a list because it's less compact, a little easier to follow. So again, we're importing it our entry points. We're gonna iterate over our skynet.commands. Now, this time what we're gonna do, so again, the goal was we had that mapping. We just want to inject some new stuff into that mapping. So that's what we're gonna do. We're going to add something to registered commands for each of our entry points. Each entry point has a name and then each entry point has a load method. Let's take a look at those. So I'm just gonna do equals next. It, does it work? Ah, no, entry points. But I know I did that, skynet.commands. Okay, I was checking to see if I had anything embarrassing in my search history, in my clipboard history. But then again, why on earth would I do that when I'm already, never mind. Okay, so let's inspect this real quick. So right now I have this entry point, help equals skynet.commands colon help command. So each entry point has a name, so we're gonna use those as our keys. So if we grab e.name, help. Okay, so that's the identifier, easy enough. And then over here we have entry point.load. Let's look at what that does, e.load. And we get the reference to the help command class. So in this way, you can take that reference to whatever it is, and you can pull it into, into the local namespace. So if I've done this right, and again I really hope I have, this is going to add each key, it's gonna be the name of that command, and each value is going to be the reference to the corresponding command class. So I just did my pip install, so I've set up all the entry points. So let's give this a shot. And there we go. There's batch score, and there's batch validate. So some of you are probably thinking, great, okay, that works wonderfully, but I'm never gonna do this. Fair enough, I mean, you're not necessarily gonna want to release a plugin architecture or an interface for your project, you may have something that's an internal app, you're just, you're not gonna do this, you're just gonna create, you're just gonna add stuff to that application. I'm gonna make one case, I'm not really gonna demo it, because it is actually quite complicated, but I'm gonna make one case where you might still wanna do this anyway. So in our case, we have a monolithic web application which has all the codes. So it has the customer portal, it has the admin interface, it has all the code for the asynchronous tasks, right? And that's fine, it works. We deploy some big beefy application servers and that handles everything just fine. However, we also have a bunch of asynchronous tasks and those asynchronous tasks are run using salary workers. Why not? Quick show of hands, who's familiar with salary? The framework, not the vegetable. A few hands went down, oh dear. Okay, good, so most of you are. Really quickly then, and if you have more questions, just talk with me after, salary is a framework for executing asynchronous tasks using dedicated worker processes. So your web app or your application server will kind of hand that off to some other process that could even be living on another VM. It's a really great way of sort of streamlining your workflow and it lets you scale out different parts of your application independently. The trick is though that when you're doing that, it's okay if the web app per se is like really massive, it has all the code, it's a massive process, maybe it takes a gig and a half of memory to run, but you don't need that many processes to handle a lot of traffic. But each one of those requests could generate a dozen different asynchronous processes and so you really want those salary workers to scale a lot more easily. So the more memory that a salary process takes, the harder it is to manage all of that. And so even in cases like this, it's actually beneficial, maybe you keep your monolithic web app running on the application servers, but then when you have your salary workers or some other process that you want to keep really lean, you just have that separate, in this case, just the SkyNet code that lives by itself and you can still use entry points to manage it. So if you want to create some interface, pick the command you want to run, your web app can still load that from the entry points, but you don't have to pull that code into your SkyNet workers. So that's in a nutshell, that's entry points, probably one of the most serially overlooked features of probably Python in general. I think I'll open it up at this point to Q&A, but before I do that, I have to show my obligatory. So Chiara, as I said, lend to EFL and there's some contact information in case you're curious. So I'll open it up for Q&A at this point. Yeah, make them work for it. So how can I word this? So the namespace of the entry points coming from multiple packages, it just appends onto there, right? Which makes sense. What happens in the case of a name collision? It's a good question. So let's say that there, in fact, let's do it, why not? So I'm gonna run over here. I love breakin' stuff, I'll tell ya. Ooh, let's have some fun. So I'm gonna call this score, right? So this conflicts with what's in the main screen, and I'm gonna give this guy multiple personality disorder. I do apologize if that was in poor taste. I would say this is why I don't improvise, but I think that would be the 12th time, so I'm just not gonna bother. All right, so I've changed setup.py. That changes the configuration and the installation instructions for my package. So I do have to redo the installation. So I'm gonna do another pip install. So I think I've done this, pip install. Yes, so I'll just redo that. And, oh, okay, well, there we go. So if you conflict within the same, sorry, I'll pull that back up. So if you conflict within the same package, entry points don't like that so much. Okay, fair enough. So I've learned my lesson, I'm really sorry, let's make this batch validate. But we'll keep this guy, I'm just curious. Let's see. So I'm gonna reinstall that guy. Oh, that worked. Interesting. And if I do a Skynet, there's our batch validate. Here's score, and this one says generate credit scores for multiple assessments at once. So that's batch. So one of them won, but here's why. So if I drop back into Python, oh dear, here we go. Wait, I know what I can do. Aha. And if I come back up here and I grab these guys, all right, let's take a look at this. I think I know what I'm doing, I hope I know what I'm doing. Yes, so notice here that as we iterate over the entry points, we get both. So here's the one from our base Skynet, and here's the one from Skynet batch. Now as it happens in our code, we iterated over them in order and added them to a dict. So clearly the one that got loaded last won. That doesn't mean that's how it has to work. So you get to choose, you can raise a warning, you can raise an exception, or you may have some logic to disambiguate. Yeah, good question. Anyone else? Okay, good, I know I'm not that good. There's gotta be questions. That's right, and then the next question is gonna be down here. This is great. You guys planned this. What's a valid entry point, because you've got, is it does it have to be like, like string equals identifier, or can you like run random Python code? That's a good question. Okay, so I don't want to encourage this, but setup.py is a Python script. So if you want to make something dependent upon some runtime variables, environment variables, or even, I don't know, connect to some website and download some code and what have you, you can certainly do that. That said, of course, the more straightforward you make it to follow, the easier some people have to use your library. That said, I did not show, so there's actually a lot more to entry points here. So for example, I could do valid.comman.run, so I could reference a method in the class, and I can even do, I can do like arg1, I think this will, let's find out if this works. I haven't done this in a while. I can do something like this, arg1 and arg3, because I can't type. And if I, I really should just, I've just created these apps. Must be in, ah, actress extras, that's how it goes. So it's not that it's like arg, I'll just do arg1 because this scares me now. Brackets, thank you, sir. One last try. Oh, I got it, okay, good. So you can tell I do this a lot, this particular thing. So if I look at these guys, so you can see this guy, you can add some extra arguments after it. And again, it's up to your code to decide what to do with that. So if that's useful, you have that flexibility. One down here. Oh, come on, you knew it was coming. I made the joke in everything. Could you please clarify how the run method gets run even though you're only specifying the class name and not a function name? Sure, so what's going on here in this case? So I'll just show you in, and it's a little bit, it's a little bit buried. So in our app.py, the entry point is this guy. So this will run. And basically what this guy does is by default, cause I'm not typing in the name of a command. It basically runs the help command, like that's the default. And what the help command is doing is it's gonna iterate over registered commands and it's gonna load the name, the key, and it's gonna find the doc string of the class. So I'll jump in here very, very quickly. So, you know, for name class in registered commands.items, and then it's just gonna grab the attributes from the doc string, this guy right here. So it's, again, it's up to your code to decide what to do with the entry point. In this case, what it's doing is it's just, oh my goodness, this is a while ago. It's just iterating over the values in that mapping. It's grabbing the key and then for the value, it's taking the doc string from the class. Now, you can do whatever you want. So in this case, you know, so if I did, actually let's do this, so if I did skynet score, I don't know, what will this do? Raise an error, that's, oh, yeah, let's avoid that one. Let's do validate. Okay, I give up. Did I revert it? Didier, didier, okay, you, go away. Okay, so like if I do skynet validate, executing the validate command. So really what's important is that in this case, skynet found the class and made the class available and then I have some code in skynet that when you pick it out of the list, it just executes a method in that class. I'll expand that very, very briefly in that said, if you did something with console scripts, so if I made, do I, little stress, so like if I did class skynet object and then I made this guy a function in that class, right? Then I could do, I can close this, and then I can do skynet.main. And if I did this correctly, no guarantees at this point because I'm way off script, but if I then type skynet, it should still work. It didn't work, no, no, no, no, okay, fair enough. What am I, yes, thank you. This guy, he's doing coding, he doesn't know how to code. Doesn't know what brackets are. Actually, that one's fine because I'm American, but anyway, okay, so you can use anything that's callable and in the case of console scripts, it will kind of just work because all it does is throw some brackets at the end of it. I hope that answered it. That was probably the longest way possible to answer your question. Hi, so the order of the concatenated list, is that just the order of the modules that were installed? So you might know the answer to this. Yeah, I'm not sure. My preference is to say that it's undefined. It might be defined, it might depend on something, but I'm very tempted to say that it's environment-specific. All right, well, if Alenda would like it, if I ended with this on the screen, so let's do that. There, okay, cool, thank you guys very much.