From ed490aa7033cbe44f2d7cee10220e48be7226280 Mon Sep 17 00:00:00 2001
From: Joe Mancuso
-
+
+
Masonite
-* * * * -
- -Not all computers are made the same so you may have some trouble installing Masonite depending on your machine. If you have any issues be sure to read the [Known Installation Issues](https://docs.masoniteproject.com/prologue/known-installation-issues) Documentation. - --* * * * -
- -**** ## Contributing -Please read the [Contributing Documentation](https://masoniteframework.gitbook.io/docs/prologue/contributing-guide) here. Development will be on the current releasing branch of the [Core Repository](https://github.com/MasoniteFramework/core) (typically the `develop` branch) so check open issues, the current Milestone and the releases in that repository. Ask any questions you like in the issues so we can have an open discussion about the framework, design decisions and future of the project. - -## Contributors - - - -Joseph Mancuso 💻 🐛 💬 🤔 |
- Vaibhav Mule 💻 🐛 💬 🤔 |
- Martín Peveri 💻 🐛 💬 🤔 |
- Tony Hammack 💻 🐛 💬 🤔 |
- Abram C. Isola 💻 🐛 💬 🤔 |
- Mitch Dennett 💻 🐛 💬 🤔 |
- Marlysson Silva 💻 🐛 💬 🤔 |
-
Christopher Byrd 💻 🐛 💬 🤔 |
- Björn Theart 💻 🐛 💬 🤔 |
- Junior Gantin 💻 🐛 💬 🤔 |
-
-* * * * -
- -NOTE: Notice this new interesting string syntax in our route. This will grant our route access to a controller (which we will create below) - --* * * * -
- -**** - -Since we used a string controller we don't have to import our controller into this file. All imports are done through Masonite on the backend. - -You'll notice that we have a reference to the `HelloWorldController` class which we do not have yet. This framework uses controllers in order to separate the application logic. Controllers can be looked at as the views.py in a Django application. The architectural standard here is 1 controller per file. - -In order to make the `HelloWorldController` we can use a `craft` command: - - $ craft controller HelloWorld - -This will scaffold the controller for you and put it in `app/http/controllers/HelloWorldController.py`. This new file will have all the imports for us. - -Inside the `HelloWorldController` we can make our `show` method like this: - -```python -def show(self, view: View): - """ Show Hello World Template """ - return view.render('helloworld') -``` - -As you see above, we are returning a `helloworld` template but we do not have that yet. All templates are in `resources/templates`. We can simply make a file called `helloworld.html` or run the `craft` command: +Contributing to Masonite is simple: - $ craft view helloworld +- Hop on [Masonite Slack Community](http://slack.masoniteproject.com/) to ask any questions you need! +- Read the [How To Contribute](https://docs.masoniteproject.com/prologue/how-to-contribute) documentation to see ways to contribute to the project. +- Read the [Contributing Guide](https://docs.masoniteproject.com/prologue/contributing-guide) to learn how to contribute to the core source code development of the project. +- [Follow Masonite Framework on Twitter](https://twitter.com/masoniteproject) to get updates about tips and tricks, announcement and releases. -Which will create the `resources/templates/helloworld.html` template for us. +## Core Maintainers -Lastly all templates run through the Jinja2 rendering engine so we can use any Jinja2 code inside our template like: +- [Joseph Mancuso](https://github.com/josephmancuso) (Author) +- [Samuel Girardin](https://github.com/girardinsamuel) +- [Marlysson Silva](https://github.com/Marlysson) -inside the `resources/views/helloworld.html` +## Sponsors -``` -{{ 'Hello World' }} -``` +To become a sponsor head to our [GitHub Sponsors page](https://github.com/sponsors/MasoniteFramework)! -Now just run the server: +## Security Vulnerabilities - $ craft serve +If you discover a security vulnerability within Masonite please read the [Security Policy](./SECURITY.md). All security vulnerabilities will be promptly addressed. -And navigate to `localhost:8000/hello/world` and you will see `Hello World` in your browser. +## License -Happy Crafting! +The Masonite framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/SECURITY.md b/SECURITY.md index 6a8084993..cb014540d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,7 +5,7 @@ The best approach to reporting a vulnerability with Masonite is to join the [Slack Channel](http://slack.masoniteproject.com) and messaged Joseph Mancuso and tell him about the issue. He will communicate this to his maintainers and get a patch ready and shipped ASAP. -You may also send an email to joe@masoniteproject.com +You may also send an email to joe@masoniteproject.com. ## LTS Versions diff --git a/WHITEPAPER.md b/WHITEPAPER.md new file mode 100644 index 000000000..2b058379b --- /dev/null +++ b/WHITEPAPER.md @@ -0,0 +1,333 @@ +# Masonite 4 White Paper + +This white paper is created to explain, in depth, how Masonite features, classes, and concepts should work. + +**Concepts and code snippets in this white paper are subject to change at anytime. This is a living document and explains how Masonite 4 currently works in its current state.** + +## Why This Paper Exists + +This white paper is intended to be a full explanation of how major parts of the system work. We will use this white paper to bring on new contributors or people interested in learning how Masonite works. + +We will use this document to stay on track and as a guide when creating new features for Masonite to stay consistent and efficient. + +Masonite 4 is the successor to Masonite 3. Masonite 4 is a complete, from the ground up rewrite of Masonite. The reason for this rewrite is plentiful: + +- Masonite was started in December of 2017 as a learning project for me to learn how frameworks work. It has since become a passion project for me and many others but there are still major parts of the framework that still have code from those beginning months. This code has obviously become legacy at this point and needs to be removed and rewritten. +- Masonite has gone through plenty of design changes over the course of 4 years and has relics in the codebase as such. We changed how routes work, added service providers, we changed how authentication works, we added web guards and many other changes. These were all sort of built around the same concepts and I think those decisions in the past have seriously stunted the growth of Masonite. Masonite at one point was one giant python file and only until version 2 did it even have service providers. +- When building a framework, one of the important concepts is making it expandable. This is done simply via new features but also done as packages and as a community. There really is no easy way to write a package for Masonite, theres no standard, theres some nice ways to plug it in but package development is not really there yet. This is why i think there are not many packages currently for Masonite. When first building Masonite I obviously didn't know what I know now. So now I am taking everything i learned over 4 years, plus everything i learned after a successful ORM project rewrite and building a Masonite framework i know will survive the test of time. +- Masonite ORM was developed recently and I am so proud of that library and how we built it that I want to apply those same principals to Masonite. Since Masonite codebase is so tightly coupled to everything it's hard to maintain it and build new features. Refactors are hard because it always leaves small remnants of technical debt left behind that eventually need to be cleaned up with a rewrite anyway. It's difficult sometimes to know which tests need to be fixed, which tests no longer apply and which tests need to be written. When you are dealing with nearly 1000 tests it gets time consuming to check them. They might be failing but how do we make them pass? Do we fix them? Do we fix the code? Do we delete the test? And even when all the tests pass again we are left with a mix of new code, refactored code and code thats there just to make the test pass. This type of time management and technical debt needed for new features is costly for open source projects. + +## Table Of Contents + +- Foundation +- Features + +## Foundation + +In M4, The foundation is completely redone. + +These improvements allows the directory structure to be anything we need it to be. All features are fully encapsulated and modular and Masonite does not need to be in specific directory structure order anymore. + +There is a new concept in M4 called the Application class. This class has is a mix between a new "Application" concept class and the container. So now everything is bound and made from the application class. It is also a callable so wsgi servers actually call this class to. Its very adaptable. + +The application class is an IOC container. So we can bind anything to these classes from key value strings to lists and dicts, to classes. Later we can make these values back out, swap them out with other implementations and other cool things. This keeps the entire framework extremely modular and really revolves around this IOC container. + +### Kernel Classes + +Kernel classes is really just a service provider that only registers things to the application class that is crucial to making the framework work. For example, we need to know where the config directories are, the view directories, controller directories, bind middleware, etc. These are booted before the service providers are even imported. These classes should not need to be developed on but will come with new applications and will be located inside those new applications. These can be tweaked per application. For example if you want your views directory to be located in `app/views` then you can do just that. + +## Providers + +Providers are a concept in Masonite in which they are simply wrappers around binding things to the container as well as logic that runs during a request. Everything will be bound to the container through a provider from mail and sessions features to fetching controller responses and showing the exception handling page. + +Providers will run 2 times. First when they are first added to the container. This runs a `register` method. The register method will bind things into the container. There is a second time it runs which is during the request which will run the `boot` method. + +Let's take the example of the route provider which contains both a `register` and `boot` method: + +```python +class RouteProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + # Register the routes + Route.set_controller_locations( + self.application.make("controllers.location") + ) + + def boot(self): + router = self.application.make("router") + request = self.application.make("request") + response = self.application.make("response") + + route = router.find(request.get_path(), request.get_request_method()) + + # Run before middleware + + if route: + Pipeline(request, response).through( + self.application.make("middleware").get_http_middleware(), + handler="before", + ) + Pipeline(request, response).through( + self.application.make("middleware").get_route_middleware(["web"]), + handler="before", + ) + + response.view(route.get_response(self.application)) + + Pipeline(request, response).through( + self.application.make("middleware").get_route_middleware(["web"]), + handler="after", + ) + Pipeline(request, response).through( + self.application.make("middleware").get_http_middleware(), + handler="after", + ) + else: + raise Exception(f"NO route found for {request.get_path()}") + +``` + +Notice that this provider is a `RouteProvider` so it registers routes as well as handles getting the response from the controller and attaching it to the response. + +So a provider can do many different things and its really not limited by anything. + +### Pipeline + +Pipeline classes run logic that needs to happen in a specific order and then cancel out if anything fails. This is perfect for request and response in the form of middleware. + +The concept is simple: we pass in 2 things into the pipe and if you want to continue, you return the first object, if you want to stop you return the second object. + +Here is an example of middleware doing this: + +```python +class VerifyCsrfToken(Middleware): + + exempt = [] + + def before(self, request, response): + + if not self.verify_token(request, self.get_token(request)): + return response.status(403) + + token = self.create_token(request) + + request.app.make("view").share( + { + "csrf_field": Markup( + f"" + ), + "csrf_token": token, + } + ) + + return request +``` + +So if the token doesn't pass we return the response which stops and exists the pipeline. Else we will continue down through to the controller. + +## Features + +Features of Masonite should be written in a very specific way. This way features are written in Masonite allow features to be: + +- expanded +- fixed +- tweaked +- provides the maximum effeciency for maintenance +- standardized features so anybody can improve features with a common understanding. + +This is a guide on how Masonite features are developed but also apply to packages as well. Packaging will be in another white paper which will be linked here: Link TBD. + +There are several moving peices to each feature. I'll explain them briefly here and then will go into detail. + +- A manager style class. This is a class that will likely be the front facing class that people use in controllers. This class will have all the drivers registered to it and be responsible for handling switching drivers, wrapping some logic, the front facing API. This is also the class that will be type hinting and "made" from the container. + - (see src/masonite/mail/Mail.py) +- driver class(es). Could be 1 or more + - (see src/masonite/drivers/mail/MailgunDriver.py) +- A service provider to register to the framework + - (see src/masonite/providers/MailProvider.py). +- Components classes when applicable. These are helper classes that wrap logic. In the ORM, think of those expression classes that wrap some logic like if a query is Raw. These classes are small encapsulated peices of functionality designed to write cleaner code in other parts of the system. Because then i would just need to do something like. So component classes help to write clean code somewhere else. very Handy. + - (see src/masonite/mail/MessageAttachment.py & src/masonite/drivers/mail/Recipient.py) +- Bindings should also be extremely simple. for mail it should be `application.make('mail')`. For sessions it should be `application.make('session')`, etc etc. +- Registers drivers to the manager in the same exact way. + (see https://github.com/MasoniteFramework/masonite4/pull/28/files#diff-221cd9f78ee5571e49f930cfd66a2229a784701de1076f132c379c81794e0ff1R18-R20) + +**I'll be using the example of building a mail feature to demonstrate how each part works together.** + +### Managers + +There are 3 parts to a manager class: + +- The manager class itself +- Drivers. +- Optional component classes. + +Managers are wrappers around your feature. Its a single entry point for your app. This is typically the class you will be type hinting. If your API looks like this: + +```python +def show(self, mail: Mail): + mail.mailable(Welcome()).send(driver="smtp") +``` + +Then this manager will have both the `mailable` and `send` methods. This is the front facing class. + +The manager is called a manager class because it manages smaller classes. These smaller classes are called drivers. + +An example manager looks something like this: + +```python +class Mail: + def __init__(self, application, driver_config=None): + self.application = application + self.drivers = {} + self.driver_config = driver_config or {} + self.options = {} + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + + def set_configuration(self, config): + self.driver_config = config + return self + + def get_driver(self, name=None): + if name is None: + return self.drivers[self.driver_config.get("default")] + return self.drivers[name] + + def get_config_options(self, driver=None): + if driver is None: + return self.driver_config[self.driver_config.get("default")] + + return self.driver_config.get(driver, {}) + + def mailable(self, mailable): + self.options = mailable.set_application(self.application).build().get_options() + return self + + def send(self, driver=None): + self.options.update(self.get_config_options(driver)) + return self.get_driver(driver).set_options(self.options).send() +``` + +The first 4 methods are really the manager boiler plates and the last 2 are the front facing methods needed to make this specific mail feature work. + +### Drivers + +Driver classes are small classes that do 1 thing and 1 thing only: do the driver logic. If this is an SMTP mail driver then the driver will be responsible for sending an email using SMTP. These driver classes **should not be responsible for anything else**. It should not be responsible for building an actual email, calling a view class to render a template, switching drivers to another driver, nothing. It should do nothing but send an email using SMTP from some kind of data structure like a dictionary of options. + +Here is an example of a driver class: + +```python +import requests +from .Recipient import Recipient + + +class MailgunDriver: + def __init__(self, application): + self.application = application + self.options = {} + + def set_options(self, options): + self.options = options + return self + + def get_mime_message(self): + data = { + "from": self.options.get("from"), + "to": Recipient(self.options.get("to")).header(), + "subject": self.options.get("subject"), + "h:Reply-To": self.options.get("reply_to"), + "html": self.options.get("html_content"), + "text": self.options.get("text_content"), + } + + if self.options.get("cc"): + data.update({"cc", self.options.get("cc")}) + if self.options.get("bcc"): + data.update({"bcc", self.options.get("bcc")}) + + return data + + def get_attachments(self): + files = [] + for attachment in self.options.get("attachments", []): + files.append(("attachment", open(attachment.path, "rb"))) + + return files + + def send(self): + domain = self.options["domain"] + secret = self.options["secret"] + attachments = self.get_attachments() + + return requests.post( + f"https://api.mailgun.net/v3/{domain}/messages", + auth=("api", secret), + data=self.get_mime_message(), + files=attachments, + ) + +``` + +Notice that theres nothing in the class above that isn't related to sending an email with mailgun. + +### Component Classes + +Component classes are another part of this relationship. A component is also a class that does 1 thing and 1 thing only. This class should have a small peice of logic **that is designed to clean up other parts of the code later down the line**. For example, if we want to compile an email from `joe@masoniteproject.com, idmann509@gmail.com` to `K}5e^$qninxvdG%TwoS4Zk-IP=v%nF!65U7P#qbV+t?@m;y`zrT|lbDZmt9 z3NQtj0!#s>08?Ns6zCKDTrQ$Wij^s5{>Ji> z-z=UjE%z=3FI;x_t}iczr_7m|NptGL@>1}eIhUWYmKRfgC(}cptn|rQQI>Q?($h-i zkkqFPh>EOh{q;jD&a#blNC|0rI!&MrI)9tS+G86VaS1I$eG=uYu0dsT(t0%4FWcCF zi%_L0D7w=?21tbVCMtwDXxa3%ZK0_lYr4|kf?ieaz3m9lD~hg?(Z?~MmvpV#uQqg| zj`lGG=%FpEtqmE_8_^1S+Q+mS`r4Q#r*&B-Hib6Yi$j7wD5Iw{E$HR6y*GjZeOi|^ zBKj}_^pdPgWbH$Aqwg0LRad1|v^Rnk^t3l*xoWsULv{%%SzXBzD^mbLaVP^VmMC7Z w)9`mxLcD_Qc-ji`*@;`xgB# z_aE2w=BRSLtLC|D4@%IF$$G7Kllv4^1Xo ^SMA!WTHZW9@{A?h! z1tTFKD3M%{G!WW^kPtMlw5n^Z#!Om?{gBI&o6?6IwT*6UN>o~!rsz^rf9lp~+vvhc z2rNArA}`Ic7pSWuPC^Q}4Xr+?)A%03MhK!~&^@XUS|tRNfY5fFgk%jyablI*hLjLE zOoZ+cN@z1iLJ)$2Do5xP6x( B;=SFFbzQH zwxP;BfP_%hET~UvfTd38<&^fD|F_Mw3yBhS`@U*3Wr#{Z_qYQmA=lj^3}9gN{^&yt zUs}Gljh#5P*T}Aj8u#7dHgMOS`TytRUyc1V`q$`|w&BQ?@b^RS`gZU}C=k@R>#zpk z>%yCBUI@0KVpJ1sOX3WHV=PNZ2|;j#FpL19Jva$L=$AB%s8EN0wdVEs)4ZW=WN?;{ zMx0u3+E5xPAuyI(=Smzx8css$z-s=Pf|L+AoUK!%P6^3K2`Rcl?zU4n2|*N!u#Uqi zp+2O9G+iYJK9fiZfgwqVaX<+@hLjL2d?I(-k0K=`!(11^&9aSNoP=bU#wWL-9<+p3 znLOBylMs0Jgk>U5xBUo4Le5+(EE6HNp@(r2g86^q1f28!zfIgV&;P$2-PSe|c`^Kp za7V}rz7Z4xWByl#JHme7%lvzM5>8#K%}$+hwG2C}dbzE%tk|!Wx+>MySSxvJ+FYC| z+o{jf3>a2yK~Jf`;{*eBz|T<)xUvWXLJuC`&ruB+CKjP%NH;2cnts4izb ww4U2dsi8>vd}< z5da3quCA~$7UQOD t8Jx0*$FoI5C1l^7ibQ~k-7)H=hjG)^vf{tJW9mWVcgb{QQBj^A{ zpjRxGs95}@L$>0_2wK1h+J_M|j}P>x;r}Nol8NN(6G8&E@)$ %Weh?$* zPJEyp|9`!3_a5f|KhmT>yO=4!6rf81TX`H`e`PECFoNET5wwmGbQUA%0gRygF@nxu z1f9kRTEhrh#Ryu#2wKJnTEYl=4@S@_jG+7Qfv(K|zr!VtwZ9VoTYO*aE79LYd)ux= zeje!vTcI~XLU1hbs{f9Ezwol}JztW)#=S$A8&gALr%LXl8TXvC=o8CVB=<>bpQMP8 zX9hBVNM53_A_Bn9o1E(jmKG2I)^w?!U@3JG0bsQ;h1EO)!0 Avq8L_UiCi1b`(-x?LZdr|Aa_52iO{Nwp9FhIg2ISyJ-|0E6-4jdWAA1BTR+ zkPg$U!;|y_23sQSRS6RTV0iJnXG?qm0bs2$({GM`!0?h(wVpUP^*p_R!4FlO6!Gv9 zR86f89Pa$TUtit-_XUp!Uh&_CQ`p87U QpbPX>;@=c6=vK x5C8_t-s>f<(hnE{#|>f4D+mBXVtCKlyG%b|$f&A$ if#XXE07I4V298ApfHOK|va7u!u1q^%=S521(ELB?y4Sq` diff --git a/package.json b/package.json deleted file mode 100644 index 1c9da95b5..000000000 --- a/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "private": true, - "scripts": { - "dev": "npx mix", - "development": "npx mix", - "watch": "npx mix watch", - "hot": "npx mix hot", - "prod": "npx mix --production", - "production": "npx mix --production" - }, - "devDependencies": { - "axios": "^0.19", - "cross-env": "^5.1", - "laravel-mix": "^4.0.7", - "lodash": "^4.17.13", - "resolve-url-loader": "^2.3.1", - "sass": "^1.15.2", - "sass-loader": "^7.1.0" - } -} diff --git a/pytest.ini b/pytest.ini index b0e5a945f..0b8b97aad 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,3 @@ [pytest] -filterwarnings = - ignore::DeprecationWarning \ No newline at end of file +markers = + integrations: only run integration tests (deselect with '-m "not integations"') \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7873dcd86..400900e18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,30 +1,29 @@ -ably==1.0.1 -bcrypt>=3.1,<3.2 -boto3==1.5.24 -cleo>=0.8,<0.9 -coveralls -cryptography>=3.4<3.5 -flake8 -hupper>=1.0,<2.0 -Jinja2>=2,<3 -masonite-events>=1.0,<2 -masonite-scheduler>=1.0.0,<=1.0.99 -https://github.com/masoniteframework/orm/archive/0.9.zip -passlib>=1.7,<1.8 -pendulum>=2.1 -pika -psutil>=5.4,<5.5 -pusher==1.7.4 -pyjwt>=1.7.1 -pypugjs==5.9.4 -python-dotenv>=0.8,<0.9 -pytest==4.6.2 -pytest-cov -requests>=2.0,<2.99 -tabulate>=0.8,<0.9 -tldextract>=2.2,<2.3 -whitenoise>=3.3 +pytest +black +cryptography +https://github.com/MasoniteFramework/orm/archive/2.0.zip +python-dotenv +waitress +responses +slackblocks +hashids +pwnedapi +argon2-cffi exceptionite -pubnub>=4.5.1 -responses==0.12.0 -mock>=3.0.5 +dotty_dict +tldextract +hupper +whitenoise +hfilesize +pusher +pytz +coverage +pytest +redis +boto3 +pusher +pymemcache +vonage +slackblocks +argon2-cffi +pwnedapi \ No newline at end of file diff --git a/resources/__init__.py b/resources/__init__.py deleted file mode 100644 index ea18e76b0..000000000 --- a/resources/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Resources directory. - -This directory is responsible for storing any resource based files such as -templates and SASS files -""" diff --git a/resources/templates/__init__.py b/resources/templates/__init__.py deleted file mode 100644 index 2e9156187..000000000 --- a/resources/templates/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Templates directory. - -This directory is responsible for storing all HTML templates for your project. -You are not required to store them here but you will need to add the environment -to the View class in a service provider if you want to use different template -environments. -""" diff --git a/resources/templates/admin_test.html b/resources/templates/admin_test.html deleted file mode 100644 index 8331c6bf2..000000000 --- a/resources/templates/admin_test.html +++ /dev/null @@ -1 +0,0 @@ -{% if user is admin %}True{% else %}False{% endif %} \ No newline at end of file diff --git a/resources/templates/auth/confirm.html b/resources/templates/auth/confirm.html deleted file mode 100644 index 9b29cca86..000000000 --- a/resources/templates/auth/confirm.html +++ /dev/null @@ -1 +0,0 @@ -confirm \ No newline at end of file diff --git a/resources/templates/auth/error.html b/resources/templates/auth/error.html deleted file mode 100644 index 760589cb5..000000000 --- a/resources/templates/auth/error.html +++ /dev/null @@ -1 +0,0 @@ -error \ No newline at end of file diff --git a/resources/templates/base.html b/resources/templates/base.html deleted file mode 100644 index aeb56db11..000000000 --- a/resources/templates/base.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - {% block title %} {{ config('application.name', 'Masonite') }} {% endblock %} - - - - - {% block css %}{% endblock %} - - - - -- -- - - diff --git a/resources/templates/csrf_field.html b/resources/templates/csrf_field.html deleted file mode 100644 index a093ad723..000000000 --- a/resources/templates/csrf_field.html +++ /dev/null @@ -1 +0,0 @@ -{{ csrf_field }} \ No newline at end of file diff --git a/resources/templates/emails/test.html b/resources/templates/emails/test.html deleted file mode 100644 index d734c5b0a..000000000 --- a/resources/templates/emails/test.html +++ /dev/null @@ -1 +0,0 @@ -testing email \ No newline at end of file diff --git a/resources/templates/filter.html b/resources/templates/filter.html deleted file mode 100644 index 4ade808f5..000000000 --- a/resources/templates/filter.html +++ /dev/null @@ -1 +0,0 @@ -{{ test|slug }} \ No newline at end of file diff --git a/resources/templates/index.html b/resources/templates/index.html deleted file mode 100644 index 2b31011cf..000000000 --- a/resources/templates/index.html +++ /dev/null @@ -1 +0,0 @@ -hey \ No newline at end of file diff --git a/resources/templates/line-statements.html b/resources/templates/line-statements.html deleted file mode 100644 index 744bd1d8d..000000000 --- a/resources/templates/line-statements.html +++ /dev/null @@ -1,3 +0,0 @@ -@if test: - {{ test }} -@endif \ No newline at end of file diff --git a/resources/templates/mail/composers.html b/resources/templates/mail/composers.html deleted file mode 100644 index c930433d9..000000000 --- a/resources/templates/mail/composers.html +++ /dev/null @@ -1 +0,0 @@ -{{ test }} \ No newline at end of file diff --git a/resources/templates/mail/share.html b/resources/templates/mail/share.html deleted file mode 100644 index c930433d9..000000000 --- a/resources/templates/mail/share.html +++ /dev/null @@ -1 +0,0 @@ -{{ test }} \ No newline at end of file diff --git a/resources/templates/mail/welcome.html b/resources/templates/mail/welcome.html deleted file mode 100644 index b7d69f714..000000000 --- a/resources/templates/mail/welcome.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - to: {{ to }} - - - -- {% block content %} - - {% endblock %} --Document - - - -- -- - diff --git a/resources/templates/mail/welcome.txt b/resources/templates/mail/welcome.txt deleted file mode 100644 index a3ba88e77..000000000 --- a/resources/templates/mail/welcome.txt +++ /dev/null @@ -1,2 +0,0 @@ -Hi {{to}} -Welcome to MasoniteFramework! diff --git a/resources/templates/pug/hello.pug b/resources/templates/pug/hello.pug deleted file mode 100644 index c06b171e4..000000000 --- a/resources/templates/pug/hello.pug +++ /dev/null @@ -1 +0,0 @@ -p hello {{ name }} \ No newline at end of file diff --git a/resources/templates/test_cache.html b/resources/templates/test_cache.html deleted file mode 100644 index c930433d9..000000000 --- a/resources/templates/test_cache.html +++ /dev/null @@ -1 +0,0 @@ -{{ test }} \ No newline at end of file diff --git a/resources/templates/test_exception.html b/resources/templates/test_exception.html deleted file mode 100644 index c930433d9..000000000 --- a/resources/templates/test_exception.html +++ /dev/null @@ -1 +0,0 @@ -{{ test }} \ No newline at end of file diff --git a/resources/templates/welcome.html b/resources/templates/welcome.html deleted file mode 100644 index 0d85f10a8..000000000 --- a/resources/templates/welcome.html +++ /dev/null @@ -1,36 +0,0 @@ -{% if exists('auth/base') %} - {% extends 'auth/base.html' %} -{% else %} - {% extends 'base.html' %} -{% endif %} - -{% block css %} - - -{% endblock %} - -{% block title %} - Welcome To {{ config('application.name', 'Masonite none') }} -{% endblock %} - -{% block content %} ---{{ to }}- - ---{% endblock %} diff --git a/routes/web.py b/routes/web.py deleted file mode 100644 index 3ced912e3..000000000 --- a/routes/web.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Web Routes.""" - -from src.masonite.routes import Get, Post, Redirect, RouteGroup, Patch, Options - - -ROUTES = [ - Get().route('/test', None).middleware('auth'), - Get('/bad', 'TestController@bad'), - Get('/welcome', 'WelcomeController@show'), - Get('/keyerror', 'TestController@keyerror'), - Get().route('/queue', 'TestController@queue'), - Options('options', 'TestController@show'), - Redirect('/redirect', 'test'), - Get().domain('test').route('/test', None).middleware('auth'), - Get().domain('test').route('/unit/test', 'TestController@testing').middleware('auth'), - Get().domain('test').route('/test/route', 'TestController@testing'), - Get('/json_response', 'TestController@json_response'), - Post('/test/post/route', 'TestController@post_test'), - Get('/login', 'TestController@testing').name('login'), - Get('/v', 'TestController@v').name('v'), - Get('/', 'TestController@v').name('v'), - Get('/test/param/@id', 'TestController@testing'), - Post('/test/json/response/@id', 'TestController@json'), - Get('/test/set/test/session', 'TestController@session'), - Get('/test/mail', 'TestController@mail'), - Get('/test/view', 'UnitTestController@view'), - Get('/test/redirect', 'UnitTestController@redirect_view'), - RouteGroup([ - Get('/test/1', 'TestController@show'), - Get('/test/2', 'TestController@show') - ], prefix='/example'), - RouteGroup([ - Get('/deep/1', 'DeepController@show'), - ], prefix='/example', namespace='subdirectory.deep.'), - RouteGroup([ - Get('/test/get', 'UnitTestController@show'), - Get('/test/param/@post_id', 'UnitTestController@param'), - Post('/test/post', 'UnitTestController@store').middleware('test'), - Get('/test/get/params', 'UnitTestController@get_params').name('get.params'), - Post('/test/params', 'UnitTestController@params'), - Post('/test/user', 'UnitTestController@user'), - Post('/test/json', 'UnitTestController@json'), - Get('/test/json/response', 'UnitTestController@response'), - Post('/test/json/validate', 'UnitTestController@validate'), - Get('/test/json/multi', 'UnitTestController@multi'), - Get('/test/json/multi_count', 'UnitTestController@multi_count'), - Patch('/test/patch', 'UnitTestController@patch'), - ], prefix="/unit") -] - -from src.masonite.auth import Auth -ROUTES += Auth.routes() diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index ee8526d3f..000000000 --- a/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[metadata] -description-file = README.md -[flake8] -ignore=E501,F401,E128,E402,E731,F821,E712 -include=masonite diff --git a/setup.py b/setup.py index 61fbc18b3..37cb4c7c6 100644 --- a/setup.py +++ b/setup.py @@ -1,99 +1,188 @@ -import os from setuptools import setup -here = os.path.abspath(os.path.dirname(__file__)) - -meta = {} -with open(os.path.join(here, "src/masonite", "__version__.py"), "r") as f: - exec(f.read(), meta) - -try: - with open("README.md", "r", encoding="utf-8") as f: - readme = f.read() -except FileNotFoundError: - readme = "" +with open("README.md", "r") as fh: + long_description = fh.read() setup( - name=meta["__title__"], + name="masonite", + # Versions should comply with PEP440. For a discussion on single-sourcing + # the version across setup.py and the project code, see + # https://packaging.python.org/en/latest/single_source_version.html + version="4.0.0b1.post6", + package_dir={"": "src"}, + description="The Masonite Framework", + long_description=long_description, + long_description_content_type="text/markdown", + # The project's main homepage. + url="https://github.com/masoniteframework/masonite", + # Author details + author="Joe Mancuso", + author_email="joe@masoniteproject.com", + # Choose your license + license="MIT", + # If your package should include things you specify in your MANIFEST.in file + # Use this option if your package needs to include files that are not python files + # like html templates or css files + include_package_data=True, + # List run-time dependencies here. These will be installed by pip when + # your project is installed. For an analysis of "install_requires" vs pip's + # requirements files see: + # https://packaging.python.org/en/latest/requirements.html + install_requires=[ + "inflection>=0.3<0.4", + "exceptionite>=1.0<1.1", + "pendulum>=2,<3", + "jinja2>=3.0.0<3.1", + "cleo>=0.8.1,<0.9", + "hupper>=1.10,<1.11", + "waitress>=1.4,<1.5", + "bcrypt>=3.2,<3.3", + "whitenoise>=5.2,<5.3", + "python-dotenv>=0.15,<0.16", + "hashids>=1.3,<1.4", + "cryptography>=3.3.1,<4.0", + "tldextract>=2.2,<2.3", + "hfilesize>=0.1", + "dotty_dict>=1.3.0<1.40", + ], + # See https://pypi.python.org/pypi?%3Aaction=list_classifiers + classifiers=[ + # How mature is this project? Common values are + # 3 - Alpha + # 4 - Beta + # 5 - Production/Stable + "Development Status :: 4 - Beta", + # Indicate who your project is intended for + "Intended Audience :: Developers", + "Topic :: Software Development :: Build Tools", + "Environment :: Web Environment", + # Pick your license as you wish (should match "license" above) + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + # Specify the Python versions you support here. In particular, ensure + # that you indicate whether you support Python 2, Python 3 or both. + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + # What does your project relate to? + keywords="Masonite, MasoniteFramework, Python, ORM", + # You can just specify the packages manually here if your project is + # simple. Or you can use find_packages(). packages=[ - "masonite.auth.guards", + "masonite", "masonite.auth", - "masonite.commands.presets", + "masonite.authentication.guards", + "masonite.authentication.models", + "masonite.authentication", + "masonite.authorization.models", + "masonite.authorization", + "masonite.broadcasting.controllers", + "masonite.broadcasting.drivers", + "masonite.broadcasting.providers", + "masonite.broadcasting", + "masonite.cache.drivers", + "masonite.cache", "masonite.commands", - "masonite.contracts.managers", - "masonite.contracts", + "masonite.configuration.providers", + "masonite.configuration", + "masonite.container", "masonite.controllers", "masonite.cookies", - "masonite.drivers.authentication", - "masonite.drivers.broadcast", - "masonite.drivers.cache", - "masonite.drivers.mail", "masonite.drivers.queue", "masonite.drivers.session", - "masonite.drivers.storage", - "masonite.drivers.upload", "masonite.drivers", + "masonite.environment", + "masonite.essentials.helpers", + "masonite.essentials.middleware", + "masonite.essentials.providers", + "masonite.essentials", + "masonite.events.commands", + "masonite.events.providers", + "masonite.events", + "masonite.exceptions", + "masonite.facades", + "masonite.filesystem.drivers", + "masonite.filesystem.providers", + "masonite.filesystem", + "masonite.foundation", + "masonite.hashing.drivers", + "masonite.hashing", "masonite.headers", "masonite.helpers", - "masonite.listeners", - "masonite.managers", + "masonite.input", + "masonite.loader", + "masonite.mail.drivers", + "masonite.mail", + "masonite.middleware.route", "masonite.middleware", + "masonite.notification.commands", + "masonite.notification.drivers.vonage", + "masonite.notification.drivers", + "masonite.notification.providers", + "masonite.notification", + "masonite.packages.providers", + "masonite.packages", + "masonite.pipeline.tasks", + "masonite.pipeline", "masonite.providers", "masonite.queues", - "masonite.testing", - "masonite", - ], - version=meta["__version__"], - install_requires=[ - "bcrypt>=3.1,<3.2", - "cleo>=0.8,<0.9", - "cryptography>=2.3<3.5", - "hupper<1.10", - "Jinja2>=2,<3", - "masonite-orm>=1.0,<1.1", - "passlib>=1.7,<1.8", - "pendulum>=2.1,<2.2", - "psutil>=5.4,<5.7", - "python-dotenv>=0.8,<0.11", - "requests>=2.0,<2.99", - "tabulate>=0.8,<0.9", - "tldextract>=2.2,<2.3", - "whitenoise>=3.3,<5", - "exceptionite>=1.0,<2", - ], - description=meta["__description__"], - long_description_content_type="text/markdown", - long_description=readme, - author=meta["__author__"], - author_email=meta["__author_email__"], - package_dir={"": "src"}, - url=meta["__url__"], - keywords=["masonite", "python web framework", "python3"], - license=meta["__licence__"], - python_requires=">=3.5", - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Web Environment", - "Framework :: Masonite", - "Intended Audience :: Developers", - "Topic :: Software Development :: Build Tools", - "Operating System :: OS Independent", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3 :: Only", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: Dynamic Content", - "Topic :: Internet :: WWW/HTTP :: WSGI", - "Topic :: Software Development :: Libraries :: Application Frameworks", - "Topic :: Software Development :: Libraries :: Python Modules", + "masonite.request", + "masonite.response", + "masonite.routes", + "masonite.scheduling.commands", + "masonite.scheduling.providers", + "masonite.scheduling", + "masonite.sessions", + "masonite.storage", + "masonite.templates", + "masonite.tests", + "masonite.utils", + "masonite.validation.commands", + "masonite.validation.providers", + "masonite.validation.resources", + "masonite.validation", + "masonite.views", ], - include_package_data=True, + # List additional groups of dependencies here (e.g. development + # dependencies). You can install these using the following syntax, + # for example: + # $ pip install -e .[dev,test] + # $ pip install your-package[dev,test] + extras_require={ + "test": [ + "coverage", + "pytest", + "redis", + "boto3", + "pusher", + "pymemcache", + "vonage", + "slackblocks", + "argon2-cffi", + "pwnedapi", + ], + }, + # If there are data files included in your packages that need to be + # installed, specify them here. If using Python 2.6 or less, then these + # have to be included in MANIFEST.in as well. + ## package_data={ + ## 'sample': [], + ## }, + # Although 'package_data' is the preferred approach, in some case you may + # need to place data files outside of your packages. See: + # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa + # In this case, 'data_file' will be installed into '- {{ config('application.name') }} -- - -
- - -/my_data' + ## data_files=[('my_data', ['data/data_file.txt'])], + # To provide executable scripts, use entry points in preference to the + # "scripts" keyword. Entry points provide cross-platform support and allow + # pip to create the appropriate form of executable for the target platform. entry_points={ "console_scripts": [ - "craft = masonite.cli:application.run", + "start = masonite.commands.Entry:application.run", ], }, ) diff --git a/src/masonite/__init__.py b/src/masonite/__init__.py index 876130766..e69de29bb 100644 --- a/src/masonite/__init__.py +++ b/src/masonite/__init__.py @@ -1,24 +0,0 @@ -from pkgutil import extend_path - -__path__ = extend_path(__path__, __name__) - - -from .managers.BroadcastManager import Broadcast -from .managers.CacheManager import Cache -from .managers.MailManager import Mail -from .managers.QueueManager import Queue -from .managers.SessionManager import Session -from .managers.UploadManager import Upload -from .environment import env -from .__version__ import ( - __title__, - __description__, - __url__, - __version__, - __author__, - __author_email__, - __licence__, - __cookie_cutter_version__, -) - -_file_source = "masonite" diff --git a/src/masonite/__version__.py b/src/masonite/__version__.py deleted file mode 100644 index f5ef6f956..000000000 --- a/src/masonite/__version__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Metadata for Masonite package.""" -__title__ = "masonite" -__description__ = "The core for the Masonite framework" -__url__ = "https://github.com/MasoniteFramework/masonite" -__version__ = "3.0.12" -__author__ = "Joseph Mancuso" -__author_email__ = "joe@masoniteproject.com" -__licence__ = "MIT" - -__cookie_cutter_version__ = "3.0" diff --git a/src/masonite/auth/Auth.py b/src/masonite/auth/Auth.py deleted file mode 100644 index ce6511491..000000000 --- a/src/masonite/auth/Auth.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Authentication Class.""" - - -class Auth: - """Facade class for the Guard class""" - - @staticmethod - def routes(): - from ..routes import Get, Post - - return [ - Get("/login", "auth.LoginController@show").name("login"), - Get("/logout", "auth.LoginController@logout").name("logout"), - Post("/login", "auth.LoginController@store"), - Get("/register", "auth.RegisterController@show").name("register"), - Post("/register", "auth.RegisterController@store"), - Get("/home", "auth.HomeController@show").name("home"), - Get("/email/verify", "auth.ConfirmController@verify_show").name("verify"), - Get("/email/verify/send", "auth.ConfirmController@send_verify_email"), - Get("/email/verify/@id:signed", "auth.ConfirmController@confirm_email"), - Get("/password", "auth.PasswordController@forget").name("forgot.password"), - Post("/password", "auth.PasswordController@send"), - Get("/password/@token/reset", "auth.PasswordController@reset").name( - "password.reset" - ), - Post("/password/@token/reset", "auth.PasswordController@update"), - ] diff --git a/src/masonite/auth/Csrf.py b/src/masonite/auth/Csrf.py deleted file mode 100644 index 98820f274..000000000 --- a/src/masonite/auth/Csrf.py +++ /dev/null @@ -1,54 +0,0 @@ -"""CSRF Protection Module.""" -import binascii -import os -from hmac import compare_digest -from .Sign import Sign -from ..exceptions import InvalidCSRFToken -from cryptography.fernet import InvalidToken - - -class Csrf: - """CSRF Protection Class.""" - - def __init__(self, request): - """CSRF constructor. - - Arguments: - request {masonite.request.Request} -- Request object - """ - self.request = request - - def generate_csrf_token(self, length=30): - """Generate CRSRF token. - - The // you see below is integer division. Since the token will be twice - the size of the length passed to. A length of 30 passed below will generate - a string length of 60 so we integer divide by 2 - - Returns: - string -- Returns token generated. - """ - token = self.request.get_cookie("MSESSID") - self.request.cookie("csrf_token", token) - return token - - def verify_csrf_token(self, token): - """Verify if csrf token is valid from the cookie set. - - Arguments: - token {string} -- The token that was generated. - - Returns: - bool - """ - try: - token = Sign().unsign(token) - except (InvalidToken, TypeError): - pass - - if self.request.get_cookie("csrf_token") and compare_digest( - self.request.get_cookie("csrf_token"), token - ): - return True - else: - return False diff --git a/src/masonite/auth/Sign.py b/src/masonite/auth/Sign.py index db442d3df..3ad3e3e8f 100644 --- a/src/masonite/auth/Sign.py +++ b/src/masonite/auth/Sign.py @@ -1,9 +1,9 @@ """Cryptographic Signing Module.""" import binascii -from cryptography.fernet import Fernet +from cryptography.fernet import Fernet, InvalidToken as CryptographyInvalidToken -from ..exceptions import InvalidSecretKey +from ..exceptions import InvalidSecretKey, InvalidToken class Sign: @@ -22,9 +22,9 @@ def __init__(self, key=None): if key: self.key = key else: - from config import application + from wsgi import application - self.key = application.KEY + self.key = application.make("key") if not self.key: raise InvalidSecretKey( @@ -70,4 +70,7 @@ def unsign(self, value=None): if not value: return f.decrypt(self.encryption).decode("utf-8") - return f.decrypt(bytes(str(value), "utf-8")).decode("utf-8") + try: + return f.decrypt(bytes(str(value), "utf-8")).decode("utf-8") + except CryptographyInvalidToken as e: + raise InvalidToken("Invalid Cryptographic Token") from e diff --git a/src/masonite/auth/__init__.py b/src/masonite/auth/__init__.py index 21f3ca23f..95f902948 100644 --- a/src/masonite/auth/__init__.py +++ b/src/masonite/auth/__init__.py @@ -1,4 +1,2 @@ -from .Auth import Auth -from .Csrf import Csrf from .Sign import Sign from .MustVerifyEmail import MustVerifyEmail diff --git a/src/masonite/auth/guards/AuthenticationGuard.py b/src/masonite/auth/guards/AuthenticationGuard.py deleted file mode 100644 index 977f94ac0..000000000 --- a/src/masonite/auth/guards/AuthenticationGuard.py +++ /dev/null @@ -1,56 +0,0 @@ -class AuthenticationGuard: - def guard(self, guard): - """Specify the guard you want to use - - Arguments: - guard {[type]} -- [description] - """ - from .Guard import Guard - - return Guard(self.app).make(guard) - - def register_guard(self, key, cls=None): - """Registers a new guard class. - - Arguments: - key {string|dict} -- The key to name the guard to a dictionary of key: values - - Keyword Arguments: - cls {object} -- A guard class. (default: {None}) - - Returns: - None - """ - from .Guard import Guard - - if isinstance(key, dict): - return Guard.guards.update(key) - - return Guard.guards.update({key: cls}) - - def register_driver(self, key, cls): - """Registers a new driver with the current guard class. - - Arguments: - key {string} -- The key to register the driver to. - cls {class} -- A guard class. - """ - self.drivers.update({key: cls}) - - def make(self, key): - """Makes a new driver from the current guard class. - - Arguments: - key {string} -- The key to for the driver to make. - - Raises: - DriverNotFound: Thrown when the driver is not registered. - - Returns: - object -- Returns a guard driver object. - """ - if key in self.drivers: - self.driver = self.app.resolve(self.drivers[key]) - return self.driver - - raise DriverNotFound("Could not find the driver {}".format(key)) diff --git a/src/masonite/auth/guards/Guard.py b/src/masonite/auth/guards/Guard.py deleted file mode 100644 index e0d1ba3bd..000000000 --- a/src/masonite/auth/guards/Guard.py +++ /dev/null @@ -1,124 +0,0 @@ -"""A Guard Class Module.""" -from ...app import App -from ...exceptions import DriverNotFound - - -class Guard: - - guards = {} - - def __init__(self, app: App): - """Guard Initializer - - Arguments: - app {masonite.app.App} -- The Masonite container - """ - self.app = app - - def make(self, key): - """Makes a guard that has been previously registered - - Arguments: - key {string} -- The key of the guard to fetch. - - Raises: - DriverNotFound: Raised when trying to fetch a guard that has not been registered yet. - - Returns: - [type] -- [description] - """ - if key in self.guards: - self._guard = self.app.resolve(self.guards[key]) - return self._guard - - raise DriverNotFound("Could not find the guard: '{}'".format(key)) - - def guard(self, key): - """Alias for the make method. - - Arguments: - key {string} -- The key of the guard to fetch. - - Returns: - masonite.guards.* -- An instance of a guard class. - """ - return self.make(key) - - def set(self, key): - """Sets the specified guard as the default guard to use. - - Arguments: - key {string} -- The key of the guard to set. - - Returns: - masonite.guards.* -- An instance of guard class. - """ - return self.make(key) - - def get(self): - """Gets the guard current class. - - Returns: - masonite.guards.* -- An instance of guard class. - """ - return self._guard - - def driver(self, key): - """Gets the driver for the currently set guard class. - - Arguments: - key {string} -- The key of the driver for the guard to get. - - Returns: - masonite.drivers.auth.* -- An auth driver class. - """ - return self._guard.make(key) - - def register_guard(self, key, cls=None): - """Registers a new guard class. - - Arguments: - key {string|dict} -- The key to name the guard to a dictionary of key: values - - Keyword Arguments: - cls {object} -- A guard class. (default: {None}) - - Returns: - None - """ - if isinstance(key, dict): - return self.guards.update(key) - - return self.guards.update({key: cls}) - - def login(self, *args, **kwargs): - """Wrapper method to call the guard class method. - - Returns: - * -- Returns what the guard class method returns. - """ - return self._guard.login(*args, **kwargs) - - def user(self, *args, **kwargs): - """Wrapper method to call the guard class method. - - Returns: - * -- Returns what the guard class method returns. - """ - return self._guard.user(*args, **kwargs) - - def register(self, *args, **kwargs): - """Wrapper method to call the guard class method. - - Returns: - * -- Returns what the guard class method returns. - """ - return self._guard.register(*args, **kwargs) - - def __getattr__(self, key, *args, **kwargs): - """Wrapper method to call the guard class methods. - - Returns: - * -- Returns what the guard class methods returns. - """ - return getattr(self._guard, key) diff --git a/src/masonite/auth/guards/WebGuard.py b/src/masonite/auth/guards/WebGuard.py deleted file mode 100644 index fe2f6b2fa..000000000 --- a/src/masonite/auth/guards/WebGuard.py +++ /dev/null @@ -1,157 +0,0 @@ -import uuid - -import bcrypt - -from ...app import App -from ...request import Request -from ...drivers import AuthCookieDriver, AuthJwtDriver -from ...helpers import config -from ...helpers import password as bcrypt_password -from .AuthenticationGuard import AuthenticationGuard - - -class WebGuard(AuthenticationGuard): - - drivers = {"cookie": AuthCookieDriver, "jwt": AuthJwtDriver} - - def __init__(self, app: App, driver=None, auth_model=None): - self.app = app - self._once = False - self.auth_model = auth_model or config("auth.auth.guards.web.model") - self.driver = self.make(driver or config("auth.auth.guards.web.driver")) - - def user(self): - """Get the currently logged in user. - - Raises: - exception -- Raised when there has been an error handling the user model. - - Returns: - object|bool -- Returns the current authenticated user object or False or None if there is none. - """ - try: - return self.driver.user(self.auth_model) - except Exception as exception: - raise exception - - return None - - def login(self, name, password): - """Login the user based on the parameters provided. - - Arguments: - name {string} -- The field to authenticate. This could be a username or email address. - password {string} -- The password to authenticate with. - - Raises: - exception -- Raised when there has been an error handling the user model. - - Returns: - object|bool -- Returns the current authenticated user object or False or None if there is none. - """ - - if not isinstance(password, str): - raise TypeError( - "Cannot login with password '{}' of type: {}".format( - password, type(password) - ) - ) - - auth_column = self.auth_model.__auth__ - - try: - # Try to login multiple or statements if given an auth list - if isinstance(auth_column, list): - model = self.auth_model.where(auth_column[0], name) - - for authentication_column in auth_column[1:]: - model.or_where(authentication_column, name) - - model = model.first() - else: - model = self.auth_model.where(auth_column, name).first() - - # MariaDB/MySQL can store the password as string - # while PostgreSQL can store it as bytes - # This is to prevent to double encode the password as bytes - password_as_bytes = self._get_password_column(model) - if not isinstance(password_as_bytes, bytes): - password_as_bytes = bytes(password_as_bytes or "", "utf-8") - - if model and bcrypt.checkpw(bytes(password, "utf-8"), password_as_bytes): - if not self._once: - remember_token = str(uuid.uuid4()) - model.remember_token = remember_token - model.save() - self.driver.save(remember_token, model=model) - self.app.make("Request").set_user(model) - return model - - except Exception as exception: - raise exception - - return False - - def logout(self): - """Logout the current authenticated user. - - Returns: - self - """ - self.driver.logout() - return self - - def login_by_id(self, user_id): - """Login a user by the user ID. - - Arguments: - user_id {string|int} -- The ID of the user model record. - - Returns: - object|False -- Returns the current authenticated user object or False or None if there is none. - """ - model = self.auth_model.find(user_id) - - if model: - if not self._once: - remember_token = str(uuid.uuid4()) - model.remember_token = remember_token - model.save() - self.driver.save(remember_token, model=model) - self.app.make("Request").set_user(model) - return model - - return False - - def once(self): - """Log in the user without saving a cookie. - - Returns: - self - """ - self._once = True - return self - - def _get_password_column(self, model): - """Gets the password column to use. - - Arguments: - model {orator.orm.Model} -- An Orator type model. - - Returns: - string - """ - if hasattr(model, "__password__"): - return getattr(model, model.__password__) - - if hasattr(model, "password"): - return getattr(model, "password") - - def register(self, user): - """Register the user. - - Arguments: - user {dict} -- A dictionary of user data information. - """ - user["password"] = bcrypt_password(user["password"]) - return self.auth_model.create(**user) diff --git a/src/masonite/auth/guards/__init__.py b/src/masonite/auth/guards/__init__.py deleted file mode 100644 index 11d537565..000000000 --- a/src/masonite/auth/guards/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .Guard import Guard -from .AuthenticationGuard import AuthenticationGuard -from .WebGuard import WebGuard diff --git a/src/masonite/authentication/Auth.py b/src/masonite/authentication/Auth.py new file mode 100644 index 000000000..e0d444aef --- /dev/null +++ b/src/masonite/authentication/Auth.py @@ -0,0 +1,155 @@ +import pendulum +import uuid +from ..routes import Route + + +class Auth: + def __init__(self, application, guard_config=None): + self.application = application + self.guards = {} + self._guard = None + self.guard_config = guard_config or {} + self.options = {} + + def add_guard(self, name, guard): + self.guards.update({name: guard}) + + def set_configuration(self, config): + self.guard_config = config + return self + + def guard(self, guard): + self._guard = guard + return self + + def get_guard(self, name=None): + if name is None and self._guard is None: + return self.guards[self.guard_config.get("default")] + + return self.guards[self._guard] + + def get_config_options(self, guard=None): + if guard is None: + options = self.guard_config.get(self.guard_config.get("default"), {}) + options.update(self.options) + return options + + options = self.guard_config.get(guard, {}) + options.update(self.options) + return options + + def attempt(self, email, password, once=False): + auth_config = self.get_config_options() + auth_config.update({"once": once}) + return self.get_guard().set_options(auth_config).attempt(email, password) + + def attempt_by_id(self, user_id, once=False): + auth_config = self.get_config_options() + auth_config.update({"once": once}) + return self.get_guard().set_options(auth_config).attempt_by_id(user_id) + + def logout(self): + """Logout the current authenticated user. + + Returns: + self + """ + self.application.make("request").remove_user() + return self.application.make("request").delete_cookie("token") + + def user(self): + """Logout the current authenticated user. + + Returns: + self + """ + auth_config = self.get_config_options() + return self.get_guard().set_options(auth_config).user() + + def register(self, dictionary): + """Logout the current authenticated user. + + Returns: + self + """ + auth_config = self.get_config_options() + return self.get_guard().set_options(auth_config).register(dictionary) + + def password_reset(self, email): + """Logout the current authenticated user. + + Returns: + self + """ + token = str(uuid.uuid4()) + try: + self.application.make("builder").new().table( + self.guard_config.get("password_reset_table") + ).create( + { + "email": email, + "token": token, + "expires_at": pendulum.now() + .add(minutes=self.guard_config.get("password_reset_expiration")) + .to_datetime_string() + if self.guard_config.get("password_reset_expiration") + else None, + "created_at": pendulum.now().to_datetime_string(), + } + ) + except Exception: + return False + + self.application.make("event").fire("auth.password_reset", email, token) + return token + + def reset_password(self, password, token): + """Logout the current authenticated user. + + Returns: + self + """ + + reset_record = ( + self.application.make("builder") + .new() + .table(self.guard_config.get("password_reset_table")) + .where("token", token) + .first() + ) + auth_config = self.get_config_options() + ( + self.get_guard() + .set_options(auth_config) + .reset_password(reset_record.get("email"), password) + ) + + ( + self.application.make("builder") + .new() + .table(self.guard_config.get("password_reset_table")) + .where("token", token) + .delete() + ) + + @classmethod + def routes(self): + return [ + Route.get("/login", "auth.LoginController@show").name("login"), + Route.get("/home", "auth.HomeController@show").name("auth.home"), + Route.get("/register", "auth.RegisterController@show").name("register"), + Route.post("/register", "auth.RegisterController@store").name("register.store"), + Route.get("/password_reset", "auth.PasswordResetController@show").name( + "password_reset" + ), + Route.post("/password_reset", "auth.PasswordResetController@store").name( + "password_reset.store" + ), + Route.get( + "/change_password", "auth.PasswordResetController@change_password" + ).name("change_password"), + Route.post( + "/change_password", "auth.PasswordResetController@store_changed_password" + ).name("change_password.store"), + Route.post("/login", "auth.LoginController@store").name("login.store") + ] diff --git a/src/masonite/authentication/__init__.py b/src/masonite/authentication/__init__.py new file mode 100644 index 000000000..14ff79940 --- /dev/null +++ b/src/masonite/authentication/__init__.py @@ -0,0 +1,2 @@ +from .Auth import Auth +from .models.authenticates import Authenticates diff --git a/src/masonite/authentication/guards/WebGuard.py b/src/masonite/authentication/guards/WebGuard.py new file mode 100644 index 000000000..fe293c361 --- /dev/null +++ b/src/masonite/authentication/guards/WebGuard.py @@ -0,0 +1,80 @@ +class WebGuard: + def __init__(self, application): + self.application = application + self.connection = None + + def set_options(self, options): + self.options = options + return self + + def attempt(self, username, password): + attempt = self.options.get("model")().attempt(username, password) + if attempt and not self.options.get("once"): + self.application.make("response").cookie("token", attempt.remember_token) + self.application.make("request").set_user(attempt) + return attempt + + def register(self, dictionary): + try: + register = self.options.get("model")().register(dictionary) + except Exception: + return False + return self.attempt_by_id(register.get_id()) + + def user(self): + """Get the currently logged in user. + + Returns: + object|bool -- Returns the current authenticated user object or False or None if there is none. + """ + token = self.application.make("request").cookie("token") + if token and self.options.get("model")(): + return ( + self.options.get("model")().where("remember_token", token).first() + or False + ) + + return False + + def attempt_by_id(self, user_id): + """Login a user by the user ID. + + Arguments: + user_id {string|int} -- The ID of the user model record. + + Returns: + object|False -- Returns the current authenticated user object or False or None if there is none. + """ + attempt = self.options.get("model")().attempt_by_id(user_id) + + if attempt and not self.options.get("once"): + self.application.make("request").cookie("token", attempt.remember_token) + self.application.make("request").set_user(attempt) + return attempt + + return False + + def reset_password(self, username, new_password): + """Login a user by the user ID. + + Arguments: + user_id {string|int} -- The ID of the user model record. + + Returns: + object|False -- Returns the current authenticated user object or False or None if there is none. + """ + attempt = self.options.get("model")().reset_password(username, new_password) + + if attempt: + return attempt + + return False + + def once(self): + """Log in the user without saving a cookie. + + Returns: + self + """ + self._once = True + return self diff --git a/src/masonite/authentication/guards/__init__.py b/src/masonite/authentication/guards/__init__.py new file mode 100644 index 000000000..47ec641de --- /dev/null +++ b/src/masonite/authentication/guards/__init__.py @@ -0,0 +1 @@ +from .WebGuard import WebGuard diff --git a/src/masonite/authentication/models/__init__.py b/src/masonite/authentication/models/__init__.py new file mode 100644 index 000000000..1fc5cebcb --- /dev/null +++ b/src/masonite/authentication/models/__init__.py @@ -0,0 +1 @@ +from .authenticates import Authenticates diff --git a/src/masonite/authentication/models/authenticates.py b/src/masonite/authentication/models/authenticates.py new file mode 100644 index 000000000..66763a232 --- /dev/null +++ b/src/masonite/authentication/models/authenticates.py @@ -0,0 +1,66 @@ +"""Password Helper Module.""" +import uuid +from ...facades import Hash + + +class Authenticates: + + __auth__ = "email" + __password__ = "password" + + def attempt(self, username, password): + """Attempts to login using a username and password""" + record = self.where(self.get_username_column(), username).first() + if not record: + return False + + record_password = getattr(record, self.get_password_column()) + if not isinstance(record_password, bytes): + record_password = bytes(record_password or "", "utf-8") + if Hash.check(password, record_password): + record.set_remember_token().save() + return record + + return False + + def register(self, dictionary): + dictionary.update( + {self.get_password_column(): Hash.make(dictionary.get("password", ""))} + ) + return self.create(dictionary) + + def get_id(self): + return self.get_primary_key_value() + + def attempt_by_id(self, user_id): + """Attempts to login using a username and password""" + record = self.find(user_id) + if not record: + return False + + record.set_remember_token().save() + return record + + def get_remember_token(self): + """Attempts to login using a username and password""" + return self.remember_token + + def set_remember_token(self, token=None): + """Attempts to login using a username and password""" + self.remember_token = str(token) if token else str(uuid.uuid4()) + return self + + def reset_password(self, username, password): + """Attempts to login using a username and password""" + self.where(self.get_username_column(), username).update( + {self.get_password_column(): Hash.make(password)} + ) + return self + + def get_password_column(self): + """Attempts to login using a username and password""" + return self.__password__ + + def get_username_column(self): + """Attempts to login using a username and password""" + return self.__auth__ diff --git a/src/masonite/authorization/AuthorizationResponse.py b/src/masonite/authorization/AuthorizationResponse.py new file mode 100644 index 000000000..cfa30e478 --- /dev/null +++ b/src/masonite/authorization/AuthorizationResponse.py @@ -0,0 +1,30 @@ +from ..exceptions.exceptions import AuthorizationException + + +class AuthorizationResponse: + def __init__(self, allowed, message="", status=None): + self._allowed = allowed + self.status = status + self._message = message + + @classmethod + def allow(cls, message="", status=None): + return cls(True, message, status) + + @classmethod + def deny(cls, message="", status=None): + return cls(False, message, status) + + def allowed(self): + return self._allowed + + def authorize(self): + if not self._allowed: + raise AuthorizationException(self._message, self.status) + return self + + def get_response(self): + return self._message, self.status + + def message(self): + return self._message diff --git a/src/masonite/authorization/AuthorizesRequest.py b/src/masonite/authorization/AuthorizesRequest.py new file mode 100644 index 000000000..ef5a3bc03 --- /dev/null +++ b/src/masonite/authorization/AuthorizesRequest.py @@ -0,0 +1,7 @@ +from ..facades import Gate + + +class AuthorizesRequest: + def authorize(self, permission, *args): + + return Gate.authorize(permission, *args) diff --git a/src/masonite/authorization/Gate.py b/src/masonite/authorization/Gate.py new file mode 100644 index 000000000..46b5298d4 --- /dev/null +++ b/src/masonite/authorization/Gate.py @@ -0,0 +1,170 @@ +from inspect import isclass, signature +from masoniteorm import Model + +from .AuthorizationResponse import AuthorizationResponse +from ..exceptions.exceptions import GateDoesNotExist, PolicyDoesNotExist + + +class Gate: + def __init__( + self, + application, + user_callback=None, + policies={}, + permissions={}, + before_callbacks=[], + after_callbacks=[], + ): + self.application = application + self.user_callback = user_callback + + self.policies = policies + self.permissions = permissions + self.before_callbacks = before_callbacks + self.after_callbacks = after_callbacks + + def define(self, permission, condition): + if not callable(condition): + raise Exception(f"Permission {permission} should be given a callable.") + + self.permissions.update({permission: condition}) + + def register_policies(self, policies): + for model_class, policy_class in policies: + self.policies[model_class] = policy_class + return self + + def get_policy_for(self, instance): + if isinstance(instance, Model): + policy = self.policies.get(instance.__class__, None) + elif isclass(instance): + policy = self.policies.get(instance, None) + elif isinstance(instance, str): + # TODO: load model from str, get class and get policies + policy = None + if policy: + return policy() + else: + return None + + def before(self, before_callback): + if not callable(before_callback): + raise Exception("before() should be given a callable.") + self.before_callbacks.append(before_callback) + + def after(self, after_callback): + if not callable(after_callback): + raise Exception("before() should be given a callable.") + self.after_callbacks.append(after_callback) + + def allows(self, permission, *args): + return self.inspect(permission, *args).allowed() + + def denies(self, permission, *args): + return not self.inspect(permission, *args).allowed() + + def has(self, permission): + return permission in self.permissions + + def for_user(self, user): + return Gate( + self.application, + lambda: user, + self.policies, + self.permissions, + self.before_callbacks, + self.after_callbacks, + ) + + def any(self, permissions, *args): + """Check that every of those permissions are allowed.""" + for permission in permissions: + if self.denies(permission, *args): + return False + return True + + def none(self, permissions, *args): + """Check that none of those permissions are allowed.""" + for permission in permissions: + if self.allows(permission, *args): + return False + return True + + def authorize(self, permission, *args): + return self.inspect(permission, *args).authorize() + + def inspect(self, permission, *args): + """Get permission checks results for the given user then builds and returns an + authorization response.""" + boolean_result = self.check(permission, *args) + if isinstance(boolean_result, AuthorizationResponse): + return boolean_result + if boolean_result: + return AuthorizationResponse.allow() + else: + return AuthorizationResponse.deny() + + def check(self, permission, *args): + """The core of the authorization class. Run before() checks, permission check and then + after() checks.""" + user = self._get_user() + + # run before checks and returns immediately if non null response + result = None + for callback in self.before_callbacks: + result = callback(user, permission) + if result: + break + + # run permission checks if nothing returned previously + if result is None: + # first check in policy + permission_method = None + if len(args) > 0: + policy = self.get_policy_for(args[0]) + if policy: + try: + permission_method = getattr(policy, permission) + except AttributeError: + raise PolicyDoesNotExist( + f"Policy method {permission} not found in {policy.__class__.__name__}." + ) + + if not permission_method: + # else check in gates + try: + permission_method = self.permissions[permission] + except KeyError: + raise GateDoesNotExist( + f"Gate {permission} has not been found. Did you declare it ?" + ) + + params = signature(permission_method).parameters + # check if user parameter is optional (meaning that guests users are allowed) + if ( + permission_method.__defaults__ + and permission_method.__defaults__[0] is None + and not user + ): + result = True + elif not user: + result = False + elif len(params) == 1: + result = permission_method(user) + else: + result = permission_method(user, *args) + + # run after checks + for callback in self.after_callbacks: + after_result = callback(user, permission, result) + result = after_result if after_result is not None else result + + return result + + def _get_user(self): + from ..facades import Request + + if self.user_callback: + return self.user_callback() + else: + return Request.user() diff --git a/src/masonite/authorization/Policy.py b/src/masonite/authorization/Policy.py new file mode 100644 index 000000000..1d66a3fc5 --- /dev/null +++ b/src/masonite/authorization/Policy.py @@ -0,0 +1,9 @@ +from .AuthorizationResponse import AuthorizationResponse + + +class Policy: + def allow(self, message="", code=None): + return AuthorizationResponse.allow(message, code) + + def deny(self, message="", code=None): + return AuthorizationResponse.deny(message, code) diff --git a/src/masonite/authorization/__init__.py b/src/masonite/authorization/__init__.py new file mode 100644 index 000000000..c24f184f3 --- /dev/null +++ b/src/masonite/authorization/__init__.py @@ -0,0 +1,5 @@ +from .AuthorizationResponse import AuthorizationResponse +from .Gate import Gate +from .Policy import Policy +from .models.authorizes import Authorizes +from .AuthorizesRequest import AuthorizesRequest diff --git a/src/masonite/authorization/models/__init__.py b/src/masonite/authorization/models/__init__.py new file mode 100644 index 000000000..0b6a76d85 --- /dev/null +++ b/src/masonite/authorization/models/__init__.py @@ -0,0 +1 @@ +from .authorizes import Authorizes diff --git a/src/masonite/authorization/models/authorizes.py b/src/masonite/authorization/models/authorizes.py new file mode 100644 index 000000000..2d5ea2243 --- /dev/null +++ b/src/masonite/authorization/models/authorizes.py @@ -0,0 +1,12 @@ +from ...facades import Gate + + +class Authorizes: + def can(self, permission, *args): + return Gate.for_user(self).allows(permission, *args) + + def cannot(self, permission, *args): + return Gate.for_user(self).denies(permission, *args) + + def can_any(self, permissions, *args): + return Gate.for_user(self).any(permissions, *args) diff --git a/src/masonite/autoload.py b/src/masonite/autoload.py deleted file mode 100644 index a4ef08ac1..000000000 --- a/src/masonite/autoload.py +++ /dev/null @@ -1,177 +0,0 @@ -"""Autoloader Module. - -This contains the class for autoloading classes from directories. -This class is simply used to point at a directory and retrieve all classes in that directory. -""" - -import inspect -import os -import pkgutil -from pydoc import importlib - -from .exceptions import AutoloadContainerOverwrite, ContainerError, InvalidAutoloadPath - - -class Autoload: - """Autoload class. Used to retrieve all classes from any set of directories.""" - - classes = {} - - def __init__(self, app=None): - """Autoload Constructor. - - Keyword Arguments: - app {masonite.app.App} -- Container class (default: {None}) - """ - self.app = app - - def load(self, directories, instantiate=False): - """Load all classes found in a list of directories into the container. - - Arguments: - directories {list} -- List of directories to search. - - Keyword Arguments: - instantiate {bool} -- Whether or not to instantiate the class (default: {False}) - - Raises: - ContainerError -- Thrown when the container is not loaded into the class. - AutoloadContainerOverwrite -- Thrown when the container already has the key binding. - """ - self.instantiate = instantiate - if not self.app: - raise ContainerError( - "Container not specified. Pass the container into the constructor" - ) - - for (module_loader, name, _) in pkgutil.iter_modules(directories): - # search_path = module_loader.path - search_path = os.path.relpath(module_loader.path) - for obj in inspect.getmembers( - self._get_module_members(module_loader, name) - ): - - # If the object is a class and the objects module starts with the search path - if inspect.isclass(obj[1]) and obj[1].__module__.split(".")[ - :-1 - ] == search_path.split("/"): - if ( - self.app.has(obj[1].__name__) - and self.app.make(obj[1].__name__) - and not self.app.make(obj[1].__name__).__module__.startswith( - search_path - ) - ): - raise AutoloadContainerOverwrite( - "Container already has the key: {}. Cannot overwrite a container key that exists outside of your application.".format( - obj[1].__name__ - ) - ) - self.app.bind(obj[1].__name__, self._can_instantiate(obj)) - - def instances(self, directories, instance, only_app=True, instantiate=False): - """Use to autoload all instances of a specific object. - - Arguments: - directories {list} -- List of directories to search. - instance {object} -- Object to search for instances of. - - Keyword Arguments: - only_app {bool} -- Only search in the current application namespace. This will not found other classes - that are imported through third party packages. (default: {True}) - instantiate {bool} -- Whether or not to instantiate the classes it finds. (default: {False}) - - Returns: - dict -- Returns a dictionary of classes it found. - """ - self.instantiate = instantiate - - for (module_loader, name, _) in pkgutil.iter_modules(directories): - # search_path = module_loader.path - search_path = os.path.relpath(module_loader.path) - for obj in inspect.getmembers( - self._get_module_members(module_loader, name) - ): - if inspect.isclass(obj[1]) and issubclass(obj[1], instance): - if only_app and obj[1].__module__.startswith( - search_path.replace("/", ".") - ): - self.classes.update( - {obj[1].__name__: self._can_instantiate(obj)} - ) - elif not only_app: - self.classes.update( - {obj[1].__name__: self._can_instantiate(obj)} - ) - - return self.classes - - def collect(self, directories, only_app=True, instantiate=False): - """Collect all classes from a specific list of directories. - - Arguments: - directories {list} -- List of directories to search. - - Keyword Arguments: - only_app {bool} -- Only search in the current application namespace. This will not found other classes - that are imported through third party packages. (default: {True}) - instantiate {bool} -- Whether or not to instantiate the classes it finds. (default: {False}) - - Returns: - dict -- Returns a dictionary of objects found and their key bindings. - """ - self.instantiate = instantiate - - for (module_loader, name, _) in pkgutil.iter_modules(directories): - # search_path = module_loader.path - search_path = os.path.relpath(module_loader.path) - - for obj in inspect.getmembers( - self._get_module_members(module_loader, name) - ): - if inspect.isclass(obj[1]): - if only_app and obj[1].__module__.startswith( - search_path.replace("/", ".") - ): - self.classes.update( - {obj[1].__name__: self._can_instantiate(obj)} - ) - elif not only_app: - self.classes.update( - {obj[1].__name__: self._can_instantiate(obj)} - ) - - return self.classes - - def _can_instantiate(self, obj): - """Instantiate the class or not depending on the property set. - - Arguments: - obj {object} -- Object to check for instantiation. - - Returns: - object -- Returns the object being instantiated. - """ - if self.instantiate: - return obj[1]() - - return obj[1] - - def _get_module_members(self, module_loader, name): - """Get the module members. - - Arguments: - module_loader {pkgutil.ModuleLoader} -- Module Loader from the pkgutil library - name {string} -- Name of the module - - Raises: - InvalidAutoloadPath -- Thrown when the search path ends with a forward - - Returns: - module -- returns the imported module. - """ - # search_path = module_loader.path - search_path = os.path.relpath(module_loader.path) - if module_loader.path.endswith("/"): - raise InvalidAutoloadPath("Autoload path cannot have a trailing slash") - return importlib.import_module(search_path.replace("/", ".") + "." + name) diff --git a/src/masonite/broadcasting/Broadcast.py b/src/masonite/broadcasting/Broadcast.py new file mode 100644 index 000000000..cbda301bb --- /dev/null +++ b/src/masonite/broadcasting/Broadcast.py @@ -0,0 +1,74 @@ +from ..routes import Route + + +class Broadcast: + def __init__(self, application, store_config=None): + self.application = application + self.drivers = {} + self.store_config = store_config or {} + self.options = {} + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + + def set_configuration(self, config): + self.store_config = config + return self + + def get_driver(self, name=None): + if name is None: + return self.drivers[self.store_config.get("default")] + return self.drivers[name] + + def driver(self, name=None): + store_config = self.get_config_options() + driver = self.get_driver(None) + return driver.set_options(store_config) + + def get_store_config(self, name=None): + if name is None or name == "default": + return self.store_config.get(self.store_config.get("default")) + + return self.store_config.get(name) + + def get_config_options(self, name=None): + if name is None or name == "default": + return self.store_config.get(self.store_config.get("default")) + + return self.store_config.get(name) + + def channel(self, channels, event=None, value=None, driver=None): + store_config = self.get_config_options() + driver = self.get_driver(driver) + if not isinstance(event, str): + if event is None: + event = channels + channels = event.broadcast_on() + + value = event.broadcast_with() + if not isinstance(channels, list): + channels = [channels] + + for channel in channels: + if not channel.authorized(self.application): + continue + event_class = event.broadcast_as() + + driver.set_options(store_config).channel( + channel.name, event_class, value + ) + else: + if not isinstance(channels, list): + channels = [channels] + for channel in channels: + driver.set_options(store_config).channel(channel, event, value) + + @classmethod + def routes(self, auth_route="/broadcasting/authorize"): + from .controllers import BroadcastingController + + return [ + Route.post(auth_route, BroadcastingController.authorize).name( + "broadcasting.authorize" + ) + ] diff --git a/src/masonite/broadcasting/CanBroadcast.py b/src/masonite/broadcasting/CanBroadcast.py new file mode 100644 index 000000000..39b15215c --- /dev/null +++ b/src/masonite/broadcasting/CanBroadcast.py @@ -0,0 +1,9 @@ +class CanBroadcast: + def broadcast_on(self): + return None + + def broadcast_with(self): + return vars(self) + + def broadcast_as(self): + return self.__class__.__name__ diff --git a/src/masonite/broadcasting/Channel.py b/src/masonite/broadcasting/Channel.py new file mode 100644 index 000000000..67e1ff5d3 --- /dev/null +++ b/src/masonite/broadcasting/Channel.py @@ -0,0 +1,6 @@ +class Channel: + def __init__(self, name): + self.name = name + + def authorized(self, application): + return True diff --git a/src/masonite/broadcasting/PresenceChannel.py b/src/masonite/broadcasting/PresenceChannel.py new file mode 100644 index 000000000..c16c8cf53 --- /dev/null +++ b/src/masonite/broadcasting/PresenceChannel.py @@ -0,0 +1,9 @@ +class PresenceChannel: + def __init__(self, name): + if not name.startswith("presence-"): + name = "presence-" + name + + self.name = name + + def authorized(self, application): + return bool(application.make("request").user()) diff --git a/src/masonite/broadcasting/PrivateChannel.py b/src/masonite/broadcasting/PrivateChannel.py new file mode 100644 index 000000000..74ecbf44f --- /dev/null +++ b/src/masonite/broadcasting/PrivateChannel.py @@ -0,0 +1,9 @@ +class PrivateChannel: + def __init__(self, name): + if not name.startswith("private-"): + name = "private-" + name + + self.name = name + + def authorized(self, application): + return bool(application.make("request").user()) diff --git a/src/masonite/broadcasting/__init__.py b/src/masonite/broadcasting/__init__.py new file mode 100644 index 000000000..4d7bf438c --- /dev/null +++ b/src/masonite/broadcasting/__init__.py @@ -0,0 +1,5 @@ +from .Channel import Channel +from .PrivateChannel import PrivateChannel +from .PresenceChannel import PresenceChannel +from .Broadcast import Broadcast +from .CanBroadcast import CanBroadcast diff --git a/src/masonite/broadcasting/controllers/BroadcastingController.py b/src/masonite/broadcasting/controllers/BroadcastingController.py new file mode 100644 index 000000000..f78cdc382 --- /dev/null +++ b/src/masonite/broadcasting/controllers/BroadcastingController.py @@ -0,0 +1,10 @@ +from ..Broadcast import Broadcast +from ...request import Request +from ...controllers import Controller + + +class BroadcastingController(Controller): + def authorize(self, request: Request, broadcast: Broadcast): + return broadcast.driver("pusher").authorize( + request.input("channel_name"), request.input("socket_id") + ) diff --git a/src/masonite/broadcasting/controllers/__init__.py b/src/masonite/broadcasting/controllers/__init__.py new file mode 100644 index 000000000..c09848264 --- /dev/null +++ b/src/masonite/broadcasting/controllers/__init__.py @@ -0,0 +1 @@ +from .BroadcastingController import BroadcastingController diff --git a/src/masonite/broadcasting/drivers/PusherDriver.py b/src/masonite/broadcasting/drivers/PusherDriver.py new file mode 100644 index 000000000..e80516cce --- /dev/null +++ b/src/masonite/broadcasting/drivers/PusherDriver.py @@ -0,0 +1,35 @@ +class PusherDriver: + def __init__(self, application): + self.application = application + self.connection = None + + def set_options(self, options): + self.options = options + return self + + def get_connection(self): + try: + import pusher + except ImportError: + raise ModuleNotFoundError( + "Could not find the 'pusher' library. Run 'pip install pusher' to fix this." + ) + + if self.connection: + return self.connection + + self.connection = pusher.Pusher( + app_id=str(self.options.get("app_id")), + key=self.options.get("client"), + secret=self.options.get("secret"), + cluster=self.options.get("cluster"), + ssl=self.options.get("ssl"), + ) + + return self.connection + + def channel(self, channel, event, value): + return self.get_connection().trigger(channel, event, value) + + def authorize(self, channel, socket_id): + return self.get_connection().authenticate(channel=channel, socket_id=socket_id) diff --git a/src/masonite/broadcasting/drivers/__init__.py b/src/masonite/broadcasting/drivers/__init__.py new file mode 100644 index 000000000..37e675f0c --- /dev/null +++ b/src/masonite/broadcasting/drivers/__init__.py @@ -0,0 +1 @@ +from .PusherDriver import PusherDriver diff --git a/src/masonite/broadcasting/providers/BroadcastProvider.py b/src/masonite/broadcasting/providers/BroadcastProvider.py new file mode 100644 index 000000000..10f2737dd --- /dev/null +++ b/src/masonite/broadcasting/providers/BroadcastProvider.py @@ -0,0 +1,20 @@ +from ...providers import Provider +from ..Broadcast import Broadcast +from ..drivers import PusherDriver +from ...configuration import config + + +class BroadcastProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + broadcast = Broadcast(self.application).set_configuration( + config("broadcast.broadcasts") + ) + broadcast.add_driver("pusher", PusherDriver(self.application)) + + self.application.bind("broadcast", broadcast) + + def boot(self): + pass diff --git a/src/masonite/broadcasting/providers/__init__.py b/src/masonite/broadcasting/providers/__init__.py new file mode 100644 index 000000000..6ac8f9ca0 --- /dev/null +++ b/src/masonite/broadcasting/providers/__init__.py @@ -0,0 +1 @@ +from .BroadcastProvider import BroadcastProvider diff --git a/src/masonite/cache/Cache.py b/src/masonite/cache/Cache.py new file mode 100644 index 000000000..d2ebe8699 --- /dev/null +++ b/src/masonite/cache/Cache.py @@ -0,0 +1,59 @@ +class Cache: + def __init__(self, application, store_config=None): + self.application = application + self.drivers = {} + self.store_config = store_config or {} + self.options = {} + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + + def set_configuration(self, config): + self.store_config = config + return self + + def get_driver(self, name=None): + if name is None: + return self.drivers[self.store_config.get("default")] + return self.drivers[name] + + def get_store_config(self, name=None): + if name is None or name == "default": + return self.store_config.get(self.store_config.get("default")) + + return self.store_config.get(name) + + def get_config_options(self, name=None): + if name is None or name == "default": + return self.store_config.get(self.store_config.get("default")) + + return self.store_config.get(name) + + def store(self, name="default"): + store_config = self.get_config_options(name) + driver = self.get_driver(self.get_config_options(name).get("driver")) + return driver.set_options(store_config) + + def add(self, *args, store=None, **kwargs): + return self.store(name=store).add(*args, **kwargs) + + def get(self, *args, store=None, **kwargs): + return self.store(name=store).get(*args, **kwargs) + + def put(self, *args, store=None, **kwargs): + return self.store(name=store).put(*args, **kwargs) + + def has(self, *args, store=None, **kwargs): + return self.store(name=store).has(*args, **kwargs) + + def forget(self, *args, store=None, **kwargs): + return self.store(name=store).forget(*args, **kwargs) + + def increment(self, *args, store=None, **kwargs): + return self.store(name=store).increment(*args, **kwargs) + + def decrement(self, *args, store=None, **kwargs): + return self.store(name=store).decrement(*args, **kwargs) + + def flush(self, *args, store=None, **kwargs): + return self.store(name=store).flush(*args, **kwargs) diff --git a/src/masonite/cache/__init__.py b/src/masonite/cache/__init__.py new file mode 100644 index 000000000..cf73978d4 --- /dev/null +++ b/src/masonite/cache/__init__.py @@ -0,0 +1 @@ +from .Cache import Cache diff --git a/src/masonite/cache/drivers/FileDriver.py b/src/masonite/cache/drivers/FileDriver.py new file mode 100644 index 000000000..fe061f80a --- /dev/null +++ b/src/masonite/cache/drivers/FileDriver.py @@ -0,0 +1,106 @@ +import os +from ...utils.filesystem import make_full_directory, modified_date +from pathlib import Path +import pendulum +import json +import glob + + +class FileDriver: + def __init__(self, application): + self.application = application + + def set_options(self, options): + self.options = options + if options.get("location"): + make_full_directory(options.get("location")) + return self + + def add(self, key, value, seconds=None): + exists = self.get(key) + if exists: + return exists + + return self.put(key, str(value), seconds=seconds) + + def get(self, key, default=None, **options): + if not self.has(key): + return None + + modified_at = self.get_modified_at(os.path.join(self._get_directory(), key)) + + with open(os.path.join(self._get_directory(), key), "r") as f: + value = f.read() + + if modified_at.add(seconds=self.get_cache_expiration(value)).is_past(): + self.forget(key) + return default + + value = self.get_value(value) + + return value + + def put(self, key, value, seconds=None, **options): + + time = self.get_expiration_time(seconds) + + if isinstance(value, (dict,)): + value = json.dumps(value) + + with open(os.path.join(self._get_directory(), key), "w") as f: + f.write(f"{time}:{value}") + + return value + + def has(self, key): + return Path(os.path.join(self._get_directory(), key)).exists() + + def increment(self, key, amount=1): + return self.put(key, str(int(self.get(key)) + amount)) + + def decrement(self, key, amount=1): + return self.put(key, str(int(self.get(key)) - amount)) + + def remember(self, key, callable): + value = self.get(key) + + if value: + return value + + callable(self) + + def forget(self, key): + try: + os.remove(os.path.join(self._get_directory(), key)) + return True + except FileNotFoundError: + return False + + def flush(self): + files = glob.glob(f"{self._get_directory()}/*") + for f in files: + os.remove(f) + + def _get_directory(self): + return self.options.get("location") + + def get_modified_at(self, filename): + return pendulum.from_timestamp(modified_date(filename)) + + def get_expiration_time(self, seconds): + if seconds is None: + seconds = 31557600 * 10 + + return seconds + + def get_value(self, value): + value = str(value.split(":", 1)[1]) + if value.isdigit(): + return str(value) + try: + return json.loads(value) + except json.decoder.JSONDecodeError: + return value + + def get_cache_expiration(self, value): + return int(value.split(":", 1)[0]) diff --git a/src/masonite/cache/drivers/MemcacheDriver.py b/src/masonite/cache/drivers/MemcacheDriver.py new file mode 100644 index 000000000..ce21e2349 --- /dev/null +++ b/src/masonite/cache/drivers/MemcacheDriver.py @@ -0,0 +1,90 @@ +import json + + +class MemcacheDriver: + def __init__(self, application): + self.application = application + self.connection = None + + def set_options(self, options): + self.options = options + return self + + def get_connection(self): + try: + from pymemcache.client.base import Client + except ImportError: + raise ModuleNotFoundError( + "Could not find the 'pymemcache' library. Run 'pip install pymemcache' to fix this." + ) + + if not self.connection: + if str(self.options.get("port")) != "0": + self.connection = Client( + f"{self.options.get('host')}:{self.options.get('port')}" + ) + else: + self.connection = Client(f"{self.options.get('host')}") + + return self.connection + + def add(self, key, value): + if self.has(key): + return self.get(key) + + self.put(key, value) + return value + + def get(self, key, default=None, **options): + if not self.has(key): + return default + + return self.get_value( + self.get_connection().get(f"{self.get_name()}_cache_{key}") + ) + + def put(self, key, value, seconds=0, **options): + if isinstance(value, (dict, list)): + value = json.dumps(value) + + return self.get_connection().set( + f"{self.get_name()}_cache_{key}", value, expire=seconds + ) + + def has(self, key): + return self.get_connection().get(f"{self.get_name()}_cache_{key}") + + def increment(self, key, amount=1): + return self.put(key, str(int(self.get(key)) + amount)) + + def decrement(self, key, amount=1): + return self.put(key, str(int(self.get(key)) - amount)) + + def remember(self, key, callable): + value = self.get(key) + + if value: + return value + + callable(self) + + def forget(self, key): + return self.get_connection().delete(f"{self.get_name()}_cache_{key}") + + def flush(self): + return self.get_connection().flush_all() + + def get_name(self): + return self.options.get("name") + + def get_value(self, value): + if isinstance(value, bytes): + value = value.decode("utf-8") + + value = str(value) + if value.isdigit(): + return str(value) + try: + return json.loads(value) + except json.decoder.JSONDecodeError: + return value diff --git a/src/masonite/cache/drivers/RedisDriver.py b/src/masonite/cache/drivers/RedisDriver.py new file mode 100644 index 000000000..eab2e7c29 --- /dev/null +++ b/src/masonite/cache/drivers/RedisDriver.py @@ -0,0 +1,95 @@ +import json + + +class RedisDriver: + def __init__(self, application): + self.application = application + self.connection = None + + def set_options(self, options): + self.options = options + return self + + def get_connection(self): + try: + import redis + except ImportError: + raise ModuleNotFoundError( + "Could not find the 'redis' library. Run 'pip install redis' to fix this." + ) + + if not self.connection: + self.connection = redis.StrictRedis( + host=self.options.get("host"), + port=self.options.get("port"), + password=self.options.get("password"), + decode_responses=True, + ) + + return self.connection + + def add(self, key, value): + if self.has(key): + return self.get(key) + + self.put(key, value) + return value + + def get(self, key, default=None, **options): + if not self.has(key): + return default + return self.get_value( + self.get_connection().get(f"{self.get_name()}_cache_{key}") + ) + + def put(self, key, value, seconds=None, **options): + + time = self.get_expiration_time(seconds) + + if isinstance(value, (dict, list)): + value = json.dumps(value) + + return self.get_connection().set( + f"{self.get_name()}_cache_{key}", value, ex=time + ) + + def has(self, key): + return self.get_connection().get(f"{self.get_name()}_cache_{key}") + + def increment(self, key, amount=1): + return self.put(key, str(int(self.get(key)) + amount)) + + def decrement(self, key, amount=1): + return self.put(key, str(int(self.get(key)) - amount)) + + def remember(self, key, callable): + value = self.get(key) + + if value: + return value + + callable(self) + + def forget(self, key): + return self.get_connection().delete(f"{self.get_name()}_cache_{key}") + + def flush(self): + return self.get_connection().flushall() + + def get_name(self): + return self.options.get("name") + + def get_expiration_time(self, seconds): + if seconds is None: + seconds = 31557600 * 10 + + return seconds + + def get_value(self, value): + value = str(value) + if value.isdigit(): + return str(value) + try: + return json.loads(value) + except json.decoder.JSONDecodeError: + return value diff --git a/src/masonite/cache/drivers/__init__.py b/src/masonite/cache/drivers/__init__.py new file mode 100644 index 000000000..5c796ed0a --- /dev/null +++ b/src/masonite/cache/drivers/__init__.py @@ -0,0 +1,3 @@ +from .FileDriver import FileDriver +from .RedisDriver import RedisDriver +from .MemcacheDriver import MemcacheDriver diff --git a/src/masonite/cli.py b/src/masonite/cli.py deleted file mode 100644 index 81e966aa1..000000000 --- a/src/masonite/cli.py +++ /dev/null @@ -1,27 +0,0 @@ -import os -import sys -from pydoc import ErrorDuringImport -from cleo import Application -from .commands import NewCommand, InstallCommand -from . import __version__ - -sys.path.append(os.getcwd()) - -application = Application("Masonite Version:", __version__) -application.add(NewCommand()) -application.add(InstallCommand()) - - -try: - from wsgi import container - from cleo import Command - - for key, value in container.collect(Command).items(): - application.add(value) -except ErrorDuringImport as e: - print(e) -except ImportError: - pass - -if __name__ == "__main__": - application.run() diff --git a/src/masonite/commands/AuthCommand.py b/src/masonite/commands/AuthCommand.py index 7fc838337..1b48f9873 100644 --- a/src/masonite/commands/AuthCommand.py +++ b/src/masonite/commands/AuthCommand.py @@ -1,59 +1,34 @@ -"""New Authentication System Command.""" +"""Scaffold Auth Command.""" +from cleo import Command +from distutils.dir_util import copy_tree import os -import shutil -from cleo import Command -from ..helpers.filesystem import make_directory +from ..utils.location import controllers_path, views_path +from ..utils.filesystem import get_module_dir class AuthCommand(Command): """ - Creates an authentication system. + Creates a new authentication scaffold. auth """ - def handle(self): - self.info("Scaffolding Application ...") - module_path = os.path.dirname(os.path.realpath(__file__)) - - with open("routes/web.py", "a") as f: - # add all the routes - f.write("\nfrom masonite.auth import Auth \n") - f.write("ROUTES += Auth.routes()") - f.write("\n") + def __init__(self, application): + super().__init__() + self.app = application - make_directory( - os.path.join(os.getcwd(), "app/http/controllers/auth/LoginController.py") + def handle(self): + copy_tree( + self.get_template_path(), + views_path("auth"), ) + copy_tree(self.get_controllers_path(), controllers_path("auth")) - # move controllers - shutil.copyfile( - module_path + "/../snippets/auth/controllers/LoginController.py", - os.getcwd() + "/app/http/controllers/auth/LoginController.py", - ) - shutil.copyfile( - module_path + "/../snippets/auth/controllers/RegisterController.py", - os.getcwd() + "/app/http/controllers/auth/RegisterController.py", - ) - shutil.copyfile( - module_path + "/../snippets/auth/controllers/HomeController.py", - os.getcwd() + "/app/http/controllers/auth/HomeController.py", - ) - shutil.copyfile( - module_path + "/../snippets/auth/controllers/ConfirmController.py", - os.getcwd() + "/app/http/controllers/auth/ConfirmController.py", - ) - shutil.copyfile( - module_path + "/../snippets/auth/controllers/PasswordController.py", - os.getcwd() + "/app/http/controllers/auth/PasswordController.py", - ) - # move templates - shutil.copytree( - module_path + "/../snippets/auth/templates/auth", - os.getcwd() + "/resources/templates/auth", - ) + self.info("Auth scaffolded successfully!") - self.info( - "Project Scaffolded. You now have 5 new controllers, 7 new templates and 9 new routes" - ) + def get_template_path(self): + return os.path.join(get_module_dir(__file__), "../stubs/templates/auth") + + def get_controllers_path(self): + return os.path.join(get_module_dir(__file__), "../stubs/controllers/auth") diff --git a/src/masonite/commands/BaseScaffoldCommand.py b/src/masonite/commands/BaseScaffoldCommand.py deleted file mode 100644 index efde70275..000000000 --- a/src/masonite/commands/BaseScaffoldCommand.py +++ /dev/null @@ -1,54 +0,0 @@ -from cleo import Command - -from ..app import App -from ..helpers.filesystem import make_directory -from ..view import View - - -class BaseScaffoldCommand(Command): - """ - Creates a model. - - model - {name : Name of the model} - """ - - scaffold_name = "Example" - suffix = "" - postfix = "" - prefix = "" - file_extension = ".py" - base_directory = "app/example/" - file_to_lower = False - - template = "/masonite/snippets/scaffold/model" - - def handle(self): - # If postfix already exists as part of the name, don't add it again - if self.postfix and self.argument("name").lower().endswith( - self.postfix.lower() - ): - class_name = self.argument("name") - else: - class_name = self.argument("name") + self.postfix - - view = View(App()) - class_directory = "{}{}{}{}".format( - self.base_directory, class_name, self.suffix, self.file_extension - ) - - if self.file_to_lower: - class_directory = class_directory.lower() - - if not make_directory(class_directory): - return self.line_error("{0} Already Exists!".format(self.scaffold_name)) - - with open(class_directory, "w+") as f: - if view.exists(self.template): - f.write( - view.render( - self.template, - {"class": self.prefix + class_name.split("/")[-1]}, - ).rendered_template - ) - self.info("{} Created Successfully!".format(self.scaffold_name)) diff --git a/src/masonite/commands/CommandCapsule.py b/src/masonite/commands/CommandCapsule.py new file mode 100644 index 000000000..6d700cac7 --- /dev/null +++ b/src/masonite/commands/CommandCapsule.py @@ -0,0 +1,12 @@ +class CommandCapsule: + def __init__(self, command_application): + self.command_application = command_application + self.commands = [] + + def add(self, *commands): + self.commands.append(commands) + self.command_application.add_commands(*commands) + return self + + def run(self): + return self.command_application.run() diff --git a/src/masonite/commands/CommandCommand.py b/src/masonite/commands/CommandCommand.py deleted file mode 100644 index d2079fdf6..000000000 --- a/src/masonite/commands/CommandCommand.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Creates New Command Command.""" -from ..commands import BaseScaffoldCommand - - -class CommandCommand(BaseScaffoldCommand): - """ - Creates a new command. - - command - {name : Name of the command you would like to create} - """ - - scaffold_name = "Command" - postfix = "Command" - template = "/masonite/snippets/scaffold/command" - base_directory = "app/commands/" diff --git a/src/masonite/commands/ControllerCommand.py b/src/masonite/commands/ControllerCommand.py deleted file mode 100644 index ec46b25a2..000000000 --- a/src/masonite/commands/ControllerCommand.py +++ /dev/null @@ -1,42 +0,0 @@ -"""New Controller Command.""" -from ..view import View -from ..app import App -from ..helpers.filesystem import make_directory - -from cleo import Command - - -class ControllerCommand(Command): - """ - Creates a controller. - - controller - {name : Name of the controller you would like to create} - {--r|--resource : Create a controller as a resource} - {--e|--exact : For add the name controller without `Controller` text} - """ - - def handle(self): - controller = self.argument("name") - view = View(App()) - - if not self.option("exact"): - controller = controller + "Controller" - - if not make_directory("app/http/controllers/{0}.py".format(controller)): - return self.line_error("{0} Controller Exists!".format(controller)) - - with open("app/http/controllers/{0}.py".format(controller), "w+") as f: - if view.exists("/masonite/snippets/scaffold/controller"): - if self.option("resource"): - template = "/masonite/snippets/scaffold/controller_resource" - else: - template = "/masonite/snippets/scaffold/controller" - - f.write( - view.render( - template, {"class": controller.split("/")[-1]} - ).rendered_template - ) - - self.info("Controller Created Successfully!") diff --git a/src/masonite/commands/DownCommand.py b/src/masonite/commands/DownCommand.py deleted file mode 100644 index 7512ccaab..000000000 --- a/src/masonite/commands/DownCommand.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Down Command.""" - -from cleo import Command - - -class DownCommand(Command): - """ - Puts the server in a maintenance state. - - down - """ - - def handle(self): - with open("bootstrap/down", "w+"): - pass diff --git a/src/masonite/commands/Entry.py b/src/masonite/commands/Entry.py new file mode 100644 index 000000000..c25347fe0 --- /dev/null +++ b/src/masonite/commands/Entry.py @@ -0,0 +1,23 @@ +"""Craft Command. + +This module is really used for backup only if the masonite CLI cannot import this for you. +This can be used by running "python craft". This module is not ran when the CLI can +successfully import commands for you. +""" + +from cleo import Application +from .ProjectCommand import ( + ProjectCommand, +) + +from .KeyCommand import KeyCommand +from .InstallCommand import InstallCommand + +application = Application("Masonite Starter Version:", 4.0) + +application.add(ProjectCommand()) +application.add(KeyCommand()) +application.add(InstallCommand()) + +if __name__ == "__main__": + application.run() diff --git a/src/masonite/commands/InfoCommand.py b/src/masonite/commands/InfoCommand.py deleted file mode 100644 index 9b382059b..000000000 --- a/src/masonite/commands/InfoCommand.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Displays Information Command.""" -import math -import os -import platform -import sys -import psutil - -from cleo import Command -from tabulate import tabulate - -from ..__version__ import __version__, __cookie_cutter_version__ - - -class InfoCommand(Command): - """ - Displays environment info for debugging. - - info - """ - - def handle(self): - from ..cli import application - - rows = [] - - rows.append(["System Information", self._get_system_info()]) - mem = math.ceil(psutil.virtual_memory().total / 1024 / 1024 / 1024.0) - rows.append(["System Memory", str(mem) + " GB"]) - rows.append(["Python Version", self._get_python_info()]) - rows.append(["Virtual Environment", self._check_virtual_environment()]) - rows.append(["Masonite Version", __version__]) - rows.append(["Cookie Cutter Version", __cookie_cutter_version__]) - - if "APP_ENV" in os.environ: - rows.append(["APP_ENV", os.environ.get("APP_ENV")]) - - if "APP_DEBUG" in os.environ: - rows.append(["APP_DEBUG", os.environ.get("APP_DEBUG")]) - - self.info("") - self.info(tabulate(rows, headers=["Environment Information", ""])) - self.info("") - - def _get_python_info(self): - py_version = platform.python_version() - py_implementation = platform.python_implementation() - return py_implementation + " " + py_version - - def _check_virtual_environment(self): - if hasattr(sys, "real_prefix") or "VIRTUAL_ENV" in os.environ: - return u"\u2713" # currently running in virtual env - return "X" - - def _get_system_info(self): - bits, _ = platform.architecture() - operating_system, _, _, _, arch, _ = platform.uname() - - if operating_system.lower() == "darwin": - operating_system = "MacOS" - return "{} {} {}".format(operating_system, arch, bits) diff --git a/src/masonite/commands/InstallCommand.py b/src/masonite/commands/InstallCommand.py index be6dca486..728a17cc0 100644 --- a/src/masonite/commands/InstallCommand.py +++ b/src/masonite/commands/InstallCommand.py @@ -28,7 +28,7 @@ def handle(self): call(["pipenv", "install"]) if not self.option("no-key"): - call(["pipenv", "shell", "craft", "key", "--store"]) + call(["pipenv", "shell", "new", "key", "--store"]) return except Exception: @@ -42,10 +42,10 @@ def handle(self): raise OSError("Could not find a Pipfile or a requirements.txt file") if not self.option("no-key"): try: - call(["craft", "key", "--store"]) + self.call("key", "--store") except Exception: self.line_error( - "Could not successfully install Masonite. This could happen for several reasons but likely because of how craft is installed on your system and you could be hitting permission issues when craft is fetching required modules." + "Could not successfully install Masonite. This could happen for several reasons but likely because of how Masonite is installed on your system and you could be hitting permission issues when Masonite is fetching required modules." " If you have correctly followed the installation instructions then you should try everything again but start inside an virtual environment first to avoid any permission issues. If that does not work then seek help in" " the Masonite Slack channel. Links can be found on GitHub in the main Masonite repo." ) diff --git a/src/masonite/commands/JobCommand.py b/src/masonite/commands/JobCommand.py deleted file mode 100644 index ef46e8baf..000000000 --- a/src/masonite/commands/JobCommand.py +++ /dev/null @@ -1,16 +0,0 @@ -"""New Job Command.""" -from ..commands import BaseScaffoldCommand - - -class JobCommand(BaseScaffoldCommand): - """ - Creates a new Job. - - job - {name : Name of the job you want to create} - """ - - scaffold_name = "Job" - template = "/masonite/snippets/scaffold/job" - base_directory = "app/jobs/" - postfix = "Job" diff --git a/src/masonite/commands/KeyCommand.py b/src/masonite/commands/KeyCommand.py index 964f7ad2a..b3cdccf12 100644 --- a/src/masonite/commands/KeyCommand.py +++ b/src/masonite/commands/KeyCommand.py @@ -22,8 +22,8 @@ def handle(self): data = file.readlines() for line_number, line in enumerate(data): - if line.startswith("KEY="): - data[line_number] = "KEY={}\n".format(key) + if line.startswith("APP_KEY="): + data[line_number] = "APP_KEY={}\n".format(key) break with open(".env", "w") as file: diff --git a/src/masonite/commands/MailableCommand.py b/src/masonite/commands/MailableCommand.py deleted file mode 100644 index 4cd12c581..000000000 --- a/src/masonite/commands/MailableCommand.py +++ /dev/null @@ -1,16 +0,0 @@ -"""New Job Command.""" -from ..commands import BaseScaffoldCommand - - -class MailableCommand(BaseScaffoldCommand): - """ - Creates a new Mailable. - - mailable - {name : Name of the job you want to create} - """ - - scaffold_name = "Mailable" - template = "/masonite/snippets/scaffold/mailable" - base_directory = "app/mailable/" - postfix = "Mailable" diff --git a/src/masonite/commands/MakeControllerCommand.py b/src/masonite/commands/MakeControllerCommand.py new file mode 100644 index 000000000..6a173d2ff --- /dev/null +++ b/src/masonite/commands/MakeControllerCommand.py @@ -0,0 +1,41 @@ +"""New Controller Command.""" +from cleo import Command +import inflection +import os + +from ..utils.location import controllers_path +from ..utils.filesystem import get_module_dir, render_stub_file + + +class MakeControllerCommand(Command): + """ + Creates a new controller class. + + controller + {name : Name of the controller} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + if not name.endswith("Controller"): + name += "Controller" + + content = render_stub_file(self.get_controllers_path(), name) + + filename = f"{name}.py" + with open(controllers_path(filename), "w") as f: + f.write(content) + + self.info(f"Controller Created ({controllers_path(filename, absolute=False)})") + + def get_template_path(self): + return os.path.join(get_module_dir(__file__), "../stubs/templates/") + + def get_controllers_path(self): + return os.path.join( + get_module_dir(__file__), "../stubs/controllers/Controller.py" + ) diff --git a/src/masonite/commands/MakeJobCommand.py b/src/masonite/commands/MakeJobCommand.py new file mode 100644 index 000000000..98a842ea6 --- /dev/null +++ b/src/masonite/commands/MakeJobCommand.py @@ -0,0 +1,38 @@ +"""New Key Command.""" +from cleo import Command +import inflection +import os + +from ..utils.filesystem import make_directory, render_stub_file, get_module_dir +from ..utils.location import jobs_path + + +class MakeJobCommand(Command): + """ + Creates a new job class. + + job + {name : Name of the job} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + content = render_stub_file(self.get_jobs_path(), name) + + filename = f"{name}.py" + filepath = jobs_path(filename) + make_directory(filepath) + + with open(filepath, "w") as f: + f.write(content) + self.info(f"Job Created ({jobs_path(filename, absolute=False)})") + + def get_template_path(self): + return os.path.join(get_module_dir(__file__), "../stubs/templates/") + + def get_jobs_path(self): + return os.path.join(get_module_dir(__file__), "../stubs/jobs/Job.py") diff --git a/src/masonite/commands/MakeMailableCommand.py b/src/masonite/commands/MakeMailableCommand.py new file mode 100644 index 000000000..d76db4d63 --- /dev/null +++ b/src/masonite/commands/MakeMailableCommand.py @@ -0,0 +1,39 @@ +"""New Mailable Command.""" +from cleo import Command +import inflection +import os + +from ..utils.filesystem import make_directory, render_stub_file, get_module_dir +from ..utils.str import as_filepath +from ..utils.location import base_path + + +class MakeMailableCommand(Command): + """ + Creates a new mailable class. + + mailable + {name : Name of the mailable} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + content = render_stub_file(self.get_mailables_path(), name) + + relative_filename = os.path.join( + as_filepath(self.app.make("mailables.location")), name + ".py" + ) + filepath = base_path(relative_filename) + make_directory(filepath) + + with open(filepath, "w") as f: + f.write(content) + + self.info(f"Mailable Created ({relative_filename})") + + def get_mailables_path(self): + return os.path.join(get_module_dir(__file__), "../stubs/mailable/Mailable.py") diff --git a/src/masonite/commands/MakePolicyCommand.py b/src/masonite/commands/MakePolicyCommand.py new file mode 100644 index 000000000..7880d7aee --- /dev/null +++ b/src/masonite/commands/MakePolicyCommand.py @@ -0,0 +1,57 @@ +"""New Policy Command.""" +import inflection +import os +from cleo import Command + +from ..utils.filesystem import make_directory + + +class MakePolicyCommand(Command): + """ + Creates a new policy class. + + policy + {name : Name of the policy} + {--model=? : Create a policy for a model with a set of predefined methods} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + if not name.endswith("Policy"): + name += "Policy" + + if self.option("model"): + with open(self.get_model_policy_path(), "r") as f: + content = f.read() + content = content.replace("__class__", name) + else: + with open(self.get_base_policy_path(), "r") as f: + content = f.read() + content = content.replace("__class__", name) + + file_name = os.path.join( + self.app.make("policies.location").replace(".", "/"), name + ".py" + ) + + make_directory(file_name) + + with open(file_name, "w") as f: + f.write(content) + self.info(f"Policy Created ({file_name})") + + def get_template_path(self): + current_path = os.path.dirname(os.path.realpath(__file__)) + + return os.path.join(current_path, "../stubs/templates/") + + def get_base_policy_path(self): + current_path = os.path.dirname(os.path.realpath(__file__)) + return os.path.join(current_path, "../stubs/policies/Policy.py") + + def get_model_policy_path(self): + current_path = os.path.dirname(os.path.realpath(__file__)) + return os.path.join(current_path, "../stubs/policies/ModelPolicy.py") diff --git a/src/masonite/commands/MakeProviderCommand.py b/src/masonite/commands/MakeProviderCommand.py new file mode 100644 index 000000000..720decefe --- /dev/null +++ b/src/masonite/commands/MakeProviderCommand.py @@ -0,0 +1,39 @@ +"""New Provider Command.""" +from cleo import Command +import inflection +import os + +from ..utils.filesystem import make_directory, render_stub_file, get_module_dir +from ..utils.str import as_filepath +from ..utils.location import base_path + + +class MakeProviderCommand(Command): + """ + Creates a new mailable class. + + provider + {name : Name of the mailable} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + + content = render_stub_file(self.get_providers_path(), name) + + relative_filename = os.path.join( + as_filepath(self.app.make("providers.location")), name + ".py" + ) + filepath = base_path(relative_filename) + make_directory(filepath) + + with open(filepath, "w") as f: + f.write(content) + self.info(f"Provider Created ({relative_filename})") + + def get_providers_path(self): + return os.path.join(get_module_dir(__file__), "../stubs/providers/Provider.py") diff --git a/src/masonite/commands/MiddlewareCommand.py b/src/masonite/commands/MiddlewareCommand.py deleted file mode 100644 index c8b7b2c5c..000000000 --- a/src/masonite/commands/MiddlewareCommand.py +++ /dev/null @@ -1,16 +0,0 @@ -"""New Middleware Command.""" -from ..commands import BaseScaffoldCommand - - -class MiddlewareCommand(BaseScaffoldCommand): - """ - Creates a middleware. - - middleware - {name : Name of the middleware} - """ - - scaffold_name = "Middleware" - suffix = "Middleware" - template = "/masonite/snippets/scaffold/middleware" - base_directory = "app/http/middleware/" diff --git a/src/masonite/commands/ModelCommand.py b/src/masonite/commands/ModelCommand.py deleted file mode 100644 index 419479fc4..000000000 --- a/src/masonite/commands/ModelCommand.py +++ /dev/null @@ -1,51 +0,0 @@ -"""New Model Command.""" -from ..app import App -from ..helpers.filesystem import make_directory -from ..view import View - -from cleo import Command - - -class ModelCommand(Command): - """ - Creates a model. - - model - {name : Name of the model} - {--m|migration : Create a migration for specified model} - {--s|seed=? : Create a database seed} - """ - - scaffold_name = "Model" - template = "/masonite/snippets/scaffold/model" - base_directory = "app/" - - def handle(self): - class_name = self.argument("name") - view = View(App()) - class_directory = "{}{}.py".format(self.base_directory, class_name) - - if not make_directory(class_directory): - return self.line_error("{0} Already Exists!".format(self.scaffold_name)) - - with open(class_directory, "w+") as f: - if view.exists(self.template): - f.write( - view.render( - self.template, {"class": class_name.split("/")[-1]} - ).rendered_template - ) - self.info("{} Created Successfully!".format(self.scaffold_name)) - - if self.option("migration"): - model_name = class_name.lower() + "s" - self.call( - "migration", - [("name", "create_{}_table".format(model_name)), ("-c", model_name)], - ) - - if self.option("seed"): - seed_file = model_name - seed_file = self.option("seed") - - self.call("seed", [("table", seed_file)]) diff --git a/src/masonite/commands/ModelDocstringCommand.py b/src/masonite/commands/ModelDocstringCommand.py deleted file mode 100644 index a5f5d4d6e..000000000 --- a/src/masonite/commands/ModelDocstringCommand.py +++ /dev/null @@ -1,34 +0,0 @@ -"""A ModelDocstringCommand Command.""" - -from cleo import Command - - -class ModelDocstringCommand(Command): - """ - Generate a model docstring based on a table definition - - model:docstring - {table : Name of the table to generate the docstring for} - {--c|connection=default : The connection to use} - """ - - def handle(self): - from config.database import DB - - if self.option("connection") == "default": - conn = DB.get_schema_manager().list_table_columns(self.argument("table")) - else: - conn = ( - DB.connection(self.option("connection")) - .get_schema_manager() - .list_table_columns(self.argument("table")) - ) - - docstring = '"""Model Definition (generated with love by Masonite) \n\n' - for name, column in conn.items(): - length = "({})".format(column._length) if column._length else "" - docstring += "{}: {}{} default: {}\n".format( - name, column.get_type(), length, column.get_default() - ) - - print(docstring + '"""') diff --git a/src/masonite/commands/PresetCommand.py b/src/masonite/commands/PresetCommand.py deleted file mode 100644 index 326e0060d..000000000 --- a/src/masonite/commands/PresetCommand.py +++ /dev/null @@ -1,71 +0,0 @@ -"""New Preset System Command.""" -from cleo import Command -from ..commands.presets.React import React -from ..commands.presets.Vue import Vue -from ..commands.presets.Vue3 import Vue3 -from ..commands.presets.Bootstrap import Bootstrap -from ..commands.presets.Remove import Remove -from ..commands.presets.Tailwind import Tailwind - - -class PresetCommand(Command): - """ - Swap the front-end scaffolding for the application - - preset - {name : Name of the preset} - """ - - def handle(self): - self.info("Scaffolding Application ...") - preset_name = self.argument("name") - presets_list = ["react", "vue", "vue3", "remove", "bootstrap", "tailwind2"] - if preset_name not in presets_list: - raise ValueError("Invalid preset. Choices are: {0}".format(presets_list)) - return getattr(self, preset_name)() - - def remove(self): - """Removes frontend scaffolding""" - Remove().install() - self.info("Frontend scaffolding removed successfully.") - - def react(self): - """Add React frontend while also removing Vue (if it was previously selected)""" - React().install() - self.info("React scaffolding installed successfully.") - self.comment( - 'Please run "npm install && npm run dev" to compile your fresh scaffolding.' - ) - - def vue(self): - """Add Vue frontend while also removing React (if it was previously selected)""" - Vue().install() - self.info("Vue scaffolding installed successfully.") - self.comment( - 'Please run "npm install && npm run dev" to compile your fresh scaffolding.' - ) - - def vue3(self): - """Add Vue 3.0 frontend while also removing React (if it was previously selected)""" - Vue3().install() - self.info("Vue 3.0 scaffolding installed successfully.") - self.comment( - 'Please run "npm install && npm run dev" to compile your fresh scaffolding.' - ) - self.comment("Then you can use the view app_vue3 as demo.") - - def bootstrap(self): - """Add Bootstrap Sass scafolding""" - Bootstrap().install() - self.info("Bootstrap scaffolding installed successfully.") - self.comment( - 'Please run "npm install && npm run dev" to compile your fresh scaffolding.' - ) - - def tailwind2(self): - """Add Tailwind CSS 2.X.""" - Tailwind().install() - self.info("Tailwind CSS 2 scaffolding installed successfully.") - self.comment( - 'Please run "npm install && npm run dev" to compile your fresh scaffolding.' - ) diff --git a/src/masonite/commands/NewCommand.py b/src/masonite/commands/ProjectCommand.py similarity index 87% rename from src/masonite/commands/NewCommand.py rename to src/masonite/commands/ProjectCommand.py index 19ff709d6..66f8860bc 100644 --- a/src/masonite/commands/NewCommand.py +++ b/src/masonite/commands/ProjectCommand.py @@ -2,6 +2,7 @@ import os import shutil import zipfile +import tempfile import requests from io import BytesIO @@ -11,14 +12,13 @@ ProjectProviderHttpError, ProjectTargetNotEmpty, ) -from .. import __cookie_cutter_version__ -class NewCommand(Command): +class ProjectCommand(Command): """ Creates a new Masonite project - new + project {target? : Path of you Masonite project} {--b|--branch=False : Specify which branch from the Masonite repo you would like to install} {--r|--release=False : Specify which version of Masonite you would like to install} @@ -29,6 +29,7 @@ class NewCommand(Command): providers = ["github", "gitlab"] # timeout in seconds for requests made to providers TIMEOUT = 20 + BRANCH = 4.0 def __init__(self, *args, **kwargs): super().__init__() @@ -51,11 +52,6 @@ def handle(self): to_dir = os.path.join(os.getcwd(), target) self.check_target_does_not_exist(to_dir) - for directory in os.listdir(os.getcwd()): - if directory.startswith("masonite-"): - return self.comment( - 'There is a folder that starts with "masonite-" and therefore craft cannot create a new project' - ) try: if repo and provider not in self.providers: return self.line_error( @@ -71,7 +67,7 @@ def handle(self): and branch == "False" and version == "False" ): - branch = __cookie_cutter_version__ + branch = self.BRANCH if branch != "False": branch_data = self.get_branch_provider_data(provider, branch) @@ -141,47 +137,46 @@ def handle(self): self.info("Crafting Application ...") + # create a tmp directory to extract project template + tmp_dir = tempfile.TemporaryDirectory() try: - # Python 3 request = requests.get(zipurl) with zipfile.ZipFile(BytesIO(request.content)) as zfile: - extracted_name = zfile.infolist()[0].filename - zfile.extractall(os.getcwd()) - success = True - except ImportError: - # Python 2 - import urllib - - r = urllib.urlopen(zipurl) - with zipfile.ZipFile(BytesIO(r.read())) as z: - extracted_name = z.infolist()[0].filename - z.extractall(os.getcwd()) - + zfile.extractall(tmp_dir.name) + extracted_path = os.path.join( + tmp_dir.name, zfile.infolist()[0].filename + ) success = True except Exception as e: self.line_error("An error occured when downloading {0}".format(zipurl)) raise e if success: - from_dir = os.path.join(os.getcwd(), extracted_name) if target == ".": - for file in os.listdir(from_dir): - shutil.move(os.path.join(from_dir, file), to_dir) - os.rmdir(from_dir) + shutil.move(extracted_path, os.getcwd()) else: - os.rename(from_dir, to_dir) + shutil.move(extracted_path, to_dir) + + # remove tmp directory + tmp_dir.cleanup() + + if target == ".": + from_dir = os.path.join(os.getcwd(), zfile.infolist()[0].filename) + + for file in os.listdir(zfile.infolist()[0].filename): + shutil.move(os.path.join(from_dir, file), os.getcwd()) + os.rmdir(from_dir) self.info("Application Created Successfully!") - self.info("Installing Dependencies ") if target == ".": + self.info("Installing Dependencies...") self.call("install") - self.info( - "Installed Successfully. Just Run `craft serve` To Start Your Application." + "Installed Successfully. Just Run `python craft serve` To Start Your Application." ) else: self.info( - "Project Created Successfully. You now will have to CD into your new '{}' directory and run `craft install` to complete the installation".format( + "You now will have to go into your new '{}' directory and run `start install` to complete the installation".format( target ) ) @@ -199,7 +194,7 @@ def check_target_does_not_exist(self, target): if os.path.isdir(target): raise ProjectTargetNotEmpty( - "{} already exists. You must craft a project in a not existing directory.".format( + "{} already exists. You must craft a project in a new directory.".format( target ) ) diff --git a/src/masonite/commands/ProviderCommand.py b/src/masonite/commands/ProviderCommand.py deleted file mode 100644 index ad0e3bd7c..000000000 --- a/src/masonite/commands/ProviderCommand.py +++ /dev/null @@ -1,16 +0,0 @@ -"""New Providers Command.""" -from ..commands import BaseScaffoldCommand - - -class ProviderCommand(BaseScaffoldCommand): - """ - Creates a new Service Provider. - - provider - {name : Name of the Service Provider you want to create} - """ - - scaffold_name = "Service Provider" - base_directory = "app/providers/" - template = "/masonite/snippets/scaffold/provider" - postfix = "Provider" diff --git a/src/masonite/commands/PublishCommand.py b/src/masonite/commands/PublishCommand.py deleted file mode 100644 index e87bbf992..000000000 --- a/src/masonite/commands/PublishCommand.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Publish Service Providers""" -from cleo import Command - - -class PublishCommand(Command): - """ - Publishes a Service Provider - - publish - {name : Name of the Service Provider you want to publish} - {--t|tag=None : The tag of the provider you want to publish} - """ - - def handle(self): - from wsgi import container - - for provider in container.make("Providers"): - if provider.__class__.__name__ == self.argument("name"): - if self.option("tag") != "None": - provider.publish(tag=self.option("tag")) - provider.publish_migrations(tag=self.option("tag")) - - provider.publish() - provider.publish_migrations() - - return - - raise ValueError("Could not find the {} provider".format(self.argument("name"))) diff --git a/src/masonite/commands/PublishPackageCommand.py b/src/masonite/commands/PublishPackageCommand.py new file mode 100644 index 000000000..acc16e4e7 --- /dev/null +++ b/src/masonite/commands/PublishPackageCommand.py @@ -0,0 +1,46 @@ +from cleo import Command + + +class PublishPackageCommand(Command): + """ + Publish package files to your project + + package:publish + {name : Name of the package} + {--r|--resources=? : Resources to publish in you project (config, views, migrations...)} + {--d|--dry=? : Just show a preview of what will be published into your project} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + from ..packages.providers import PackageProvider + + name = self.argument("name") + selected_provider = None + for provider in self.app.get_providers(): + if isinstance(provider, PackageProvider) and provider.package.name == name: + selected_provider = provider + if not selected_provider: + self.line_error( + f"No package has been registered under the name {name}.", style="error" + ) + return + + if self.option("resources"): + resources = self.option("resources").split(",") + else: + resources = None + dry = self.option("dry") + published_resources = selected_provider.publish(resources, dry) + if dry: + self.info("The following files would be published:") + else: + self.info("The following files have been published:") + for resource, files in published_resources.items(): + self.info("\n") + self.info(f"{resource.capitalize()}:") + for f in files: + self.info(f" - {f}") diff --git a/src/masonite/commands/QueueFailedCommand.py b/src/masonite/commands/QueueFailedCommand.py new file mode 100644 index 000000000..34da72d05 --- /dev/null +++ b/src/masonite/commands/QueueFailedCommand.py @@ -0,0 +1,33 @@ +"""New Key Command.""" +from cleo import Command +import os + +from ..utils.filesystem import make_directory, get_module_dir +from ..utils.time import migration_timestamp +from ..utils.location import base_path + + +class QueueFailedCommand(Command): + """ + Creates a failed jobs table + + queue:failed + {--d|--directory=databases/migrations : Specifies the directory to create the migration in} + """ + + def handle(self): + with open( + os.path.join( + get_module_dir(__file__), "stubs/queue/create_failed_jobs_table.py" + ) + ) as fp: + output = fp.read() + + filename = f"{migration_timestamp()}_create_failed_jobs_table.py" + path = os.path.join(base_path(self.option("directory")), filename) + make_directory(path) + + with open(path, "w") as fp: + fp.write(output) + + self.info(f"Migration file created: {filename}") diff --git a/src/masonite/commands/QueueRetryCommand.py b/src/masonite/commands/QueueRetryCommand.py new file mode 100644 index 000000000..5c6d0de3c --- /dev/null +++ b/src/masonite/commands/QueueRetryCommand.py @@ -0,0 +1,28 @@ +"""New Key Command.""" +from cleo import Command + + +class QueueRetryCommand(Command): + """ + Puts all failed queue jobs back onto the queue. + + queue:retry + {--c|--connection=default : Specifies the database connection if using database driver.} + {--queue=default : The queue to listen to} + {--d|driver=None : Specify the driver you would like to connect to} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + driver = None if self.option("driver") == "None" else self.option("driver") + + return self.app.make("queue").retry( + { + "driver": driver, + "connection": self.option("connection"), + "queue": self.option("queue"), + } + ) diff --git a/src/masonite/commands/QueueTableCommand.py b/src/masonite/commands/QueueTableCommand.py index 59155e365..11b9ca92f 100644 --- a/src/masonite/commands/QueueTableCommand.py +++ b/src/masonite/commands/QueueTableCommand.py @@ -1,26 +1,36 @@ -"""A QueueTableCommand Command""" - - +"""New Queue Table Command.""" from cleo import Command +import os -from ..helpers.filesystem import copy_migration +from ..utils.filesystem import make_directory, get_module_dir +from ..utils.time import migration_timestamp +from ..utils.location import base_path class QueueTableCommand(Command): """ - Create migration files for the queue feature + Creates the jobs table queue:table - {--failed : Created the queue failed table} - {--jobs : Created the queue failed table} + {--d|--directory=databases/migrations : Specifies the directory to create the migration in} """ def handle(self): - if self.option("failed"): - copy_migration("masonite/snippets/migrations/create_failed_jobs_table.py") - self.info("Failed queue table migration created successfully") - if self.option("jobs"): - copy_migration("masonite/snippets/migrations/create_queue_jobs_table.py") - self.info("Jobs queue table migration created successfully") - - self.line(" Please specify the --failed or --jobs flags ") + with open( + os.path.join( + get_module_dir(__file__), "../stubs/queue/create_queue_jobs_table.py" + ) + ) as fp: + output = fp.read() + + relative_filename = os.path.join( + self.option("directory"), + f"{migration_timestamp()}_create_queue_jobs_table.py", + ) + filepath = base_path(relative_filename) + make_directory(filepath) + + with open(filepath, "w") as fp: + fp.write(output) + + self.info(f"Migration file created: {relative_filename}") diff --git a/src/masonite/commands/QueueWorkCommand.py b/src/masonite/commands/QueueWorkCommand.py index ecb5af2f3..66332ac99 100644 --- a/src/masonite/commands/QueueWorkCommand.py +++ b/src/masonite/commands/QueueWorkCommand.py @@ -1,38 +1,31 @@ -"""A QueueWorkCommand Command.""" - +"""Queue Work Command.""" from cleo import Command -from .. import Queue - class QueueWorkCommand(Command): """ - Start the queue worker + Creates a new queue worker to consume queue jobs queue:work - {--c|channel=default : The channel to listen on the queue} - {--queue=default : The queue to listen to} - {--d|driver=default : Specify the driver you would like to connect to} - {--f|fair : Send jobs to queues that have no jobs instead of randomly selecting a queue} - {--p|poll=0 : Specify the amount of time a worker should poll} - {--failed : Run only the failed jobs} + {--c|--connection : Specifies the database connection if using database driver.} + {--queue=? : The queue to listen to} + {--d|driver=? : Specify the driver you would like to use} + {--p|poll=? : Specify the seconds a worker should wait before fetching new jobs} + {--attempts=? : Specify the number of times a job should be retried before it fails} """ - def handle(self): - from wsgi import container + def __init__(self, application): + super().__init__() + self.app = application - if self.option("driver") == "default": - queue = container.make(Queue) - else: - queue = container.make(Queue).driver(self.option("driver")) + def handle(self): + options = {} + options.update({"driver": self.option("driver")}) + options.update({"poll": self.option("poll") or "1"}) + options.update({"attempts": self.option("attempts") or "3"}) + options.update({"queue": self.option("queue") or "default"}) - if self.option("failed"): - queue.run_failed_jobs() - return + if self.option("verbose"): + options.update({"verbosity": "v" + self.option("verbose")}) - queue.connect().consume( - self.option("channel"), - fair=self.option("fair"), - poll=self.option("poll"), - queue=self.option("queue"), - ) + return self.app.make("queue").consume(options) diff --git a/src/masonite/commands/RoutesCommand.py b/src/masonite/commands/RoutesCommand.py deleted file mode 100644 index 093a3228f..000000000 --- a/src/masonite/commands/RoutesCommand.py +++ /dev/null @@ -1,31 +0,0 @@ -"""List Routes Command.""" -from cleo import Command -from tabulate import tabulate - - -class RoutesCommand(Command): - """ - List out all routes of the application. - - show:routes - """ - - def handle(self): - from wsgi import container - - web_routes = container.make("WebRoutes") - - routes = [["Method", "Path", "Name", "Domain", "Middleware"]] - - for route in web_routes: - routes.append( - [ - route.method_type, - route.route_url, - route.named_route, - route.required_domain, - ",".join(route.list_middleware), - ] - ) - - print(tabulate(routes, headers="firstrow", tablefmt="rst")) diff --git a/src/masonite/commands/SeedCommand.py b/src/masonite/commands/SeedCommand.py deleted file mode 100644 index 99e93ac8e..000000000 --- a/src/masonite/commands/SeedCommand.py +++ /dev/null @@ -1,31 +0,0 @@ -"""New Seeder Command.""" -import subprocess -import os - -from cleo import Command - - -class SeedCommand(Command): - """ - Create a seeder to seed a database. - - seed - {table : Name of the table to seed} - """ - - def handle(self): - table = self.argument("table").lower() - subprocess.call( - ["orator make:seed {}_table_seeder -p databases/seeds".format(table)], - shell=True, - ) - - self.check_init_file() - - def check_init_file(self): - os.makedirs(os.path.dirname("databases/seeds/__init__.py"), exist_ok=True) - - with open("databases/seeds/__init__.py") as f: - if "sys.path.append(os.getcwd())" not in f.read(): - with open("databases/seeds/__init__.py", "w+") as fp: - fp.write("import os\nimport sys\nsys.path.append(os.getcwd())\n") diff --git a/src/masonite/commands/SeedRunCommand.py b/src/masonite/commands/SeedRunCommand.py deleted file mode 100644 index 119add642..000000000 --- a/src/masonite/commands/SeedRunCommand.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Run Seed Command.""" -import subprocess - -from cleo import Command - - -class SeedRunCommand(Command): - """ - Run seed for database. - - seed:run - {table=None : Name of the table to seed} - """ - - def handle(self): - - table = self.argument("table").lower() - if not table == "none": - seeder = "--seeder {}_table_seeder".format(table.lower()) - else: - seeder = "" - - subprocess.call( - [ - "orator db:seed -p databases/seeds -c config/database.py -f {}".format( - seeder - ), - ], - shell=True, - ) diff --git a/src/masonite/commands/ServeCommand.py b/src/masonite/commands/ServeCommand.py index 41fd11c52..03f977b70 100644 --- a/src/masonite/commands/ServeCommand.py +++ b/src/masonite/commands/ServeCommand.py @@ -1,11 +1,8 @@ -import time -import os +import sys -from hupper.logger import DefaultLogger, LogLevel -from hupper.reloader import Reloader, find_default_monitor_factory +import hupper +import waitress from cleo import Command -from ..exceptions import DriverLibraryNotFound -from ..helpers import has_unmigrated_migrations class ServeCommand(Command): @@ -21,25 +18,22 @@ class ServeCommand(Command): {--l|live-reload : Make the server automatically refresh your web browser} """ - def handle(self): - if has_unmigrated_migrations(): - self.comment( - "\nYou have unmigrated migrations. Run 'craft migrate' to migrate them\n" - ) + def __init__(self, application): + super().__init__() + self.app = application + def handle(self): if self.option("live-reload"): try: from livereload import Server except ImportError: - raise DriverLibraryNotFound( + raise ImportError( "Could not find the livereload library. Install it by running 'pip install livereload==2.5.1'" ) - from wsgi import container - from config import application import glob - server = Server(container.make("WSGI")) + server = Server(self.app) for filepath in glob.glob("resources/templates/**/*/"): server.watch(filepath) @@ -49,52 +43,33 @@ def handle(self): "This will only work for templates. Changes to Python files may require a browser refresh." ) self.line("") - application = server.serve( + server.serve( port=self.option("port"), restart_delay=self.option("reload-interval"), liveport=5500, - root=application.BASE_DIRECTORY, + root=self.app.base_path, debug=True, ) return - if not self.option("dont-reload"): - logger = DefaultLogger(LogLevel.INFO) + reloader = hupper.start_reloader(self.app.make("server.runner")) - # worker args are pickled and then passed to the new process - worker_args = [ - self.option("host"), - self.option("port"), - "wsgi:application", - ] - - reloader = Reloader( - "masonite.commands._devserver.run", - find_default_monitor_factory(logger), - logger, - worker_args=worker_args, - ) + # monitor an extra file + reloader.watch_files([".env", self.app.get_storage_path()]) - self._run_reloader(reloader, extra_files=[".env", "storage/"]) - else: - from wsgi import application - from ._devserver import run +def main(args=sys.argv[1:]): + from wsgi import application - run(self.option("host"), self.option("port"), application) + host = "127.0.0.1" + port = "8000" + if "--host" in args: + host = args[args.index("--host") + 1] + if "--port" in args: + port = args[args.index("--host") + 1] + if "-p" in args: + port = args[args.index("-p") + 1] - def _run_reloader(self, reloader, extra_files=[]): - reloader._capture_signals() - reloader._start_monitor() - for blob in extra_files: - reloader.monitor.add_path(os.path.join(os.getcwd(), blob)) - try: - while True: - if not reloader._run_worker(): - reloader._wait_for_changes() - time.sleep(float(self.option("reload-interval"))) - except KeyboardInterrupt: - pass - finally: - reloader._stop_monitor() - reloader._restore_signals() + waitress.serve( + application, host=host, port=port, clear_untrusted_proxy_headers=False + ) diff --git a/src/masonite/commands/TestCommand.py b/src/masonite/commands/TestCommand.py deleted file mode 100644 index f712a2164..000000000 --- a/src/masonite/commands/TestCommand.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Creates New Test Command.""" -from ..commands import BaseScaffoldCommand - - -class TestCommand(BaseScaffoldCommand): - """ - Creates a new test case. - - test - {name : Name of the test you would like to create} - """ - - scaffold_name = "Test" - postfix = "" - prefix = "Test" - template = "/masonite/snippets/scaffold/test" - base_directory = "tests/test_" - file_to_lower = True diff --git a/src/masonite/commands/TinkerCommand.py b/src/masonite/commands/TinkerCommand.py index 412ef8b46..cabc83a83 100644 --- a/src/masonite/commands/TinkerCommand.py +++ b/src/masonite/commands/TinkerCommand.py @@ -1,12 +1,21 @@ """Starts Interactive Console Command.""" import code import sys - from cleo import Command -BANNER = """Masonite Python {} Console +from ..environment import env +from ..utils.collections import collect +from ..utils.structures import load, data_get +from ..utils.location import base_path, config_path +from ..helpers import optional, url +from ..facades import Loader + + +BANNER = """Masonite Python \033[92m {} \033[0m Console This interactive console has the following things imported: - container as 'app' + -\033[92m app(container), \033[0m + - Utils:\033[92m {}, \033[0m + - Models:\033[92m {}, \033[0m Type `exit()` to exit.""" @@ -16,14 +25,48 @@ class TinkerCommand(Command): Run a python shell with the container pre-loaded. tinker + {--i|ipython : Run a IPython shell} """ def handle(self): - from wsgi import container + from wsgi import application + from masoniteorm.models import Model version = "{}.{}.{}".format( sys.version_info.major, sys.version_info.minor, sys.version_info.micro ) - banner = BANNER.format(version) + models = Loader.find_all(Model, "tests/integrations/app") + banner = BANNER.format( + version, + "env, optional, load, collect, url, asset, route, load, data_get, base_path, config_path", + ",".join(models.keys()), + ) + helpers = { + "app": application, + "env": env, + "optional": optional, + "collect": collect, + "url": url.url, + "asset": url.asset, + "route": url.route, + "load": load, + "data_get": data_get, + "base_path": base_path, + "config_path": config_path, + } + context = {**helpers, **models} + + if self.option("ipython"): + try: + import IPython + except ImportError: + raise ModuleNotFoundError( + "Could not find the 'IPython' library. Run 'pip install ipython' to fix this." + ) + from traitlets.config import Config - code.interact(banner=banner, local={"app": container}) + c = Config() + c.TerminalInteractiveShell.banner1 = banner + IPython.start_ipython(argv=[], user_ns=context, config=c) + else: + code.interact(banner=banner, local=context) diff --git a/src/masonite/commands/UpCommand.py b/src/masonite/commands/UpCommand.py deleted file mode 100644 index 5377ee8fd..000000000 --- a/src/masonite/commands/UpCommand.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Up Command.""" - -import os - -from cleo import Command - - -class UpCommand(Command): - """ - Brings the server out of maintenance state. - - up - """ - - def handle(self): - os.remove("bootstrap/down") diff --git a/src/masonite/commands/ViewCommand.py b/src/masonite/commands/ViewCommand.py deleted file mode 100644 index 186e7cf27..000000000 --- a/src/masonite/commands/ViewCommand.py +++ /dev/null @@ -1,16 +0,0 @@ -"""New View Command.""" -from ..commands import BaseScaffoldCommand - - -class ViewCommand(BaseScaffoldCommand): - """ - Creates a view. - - view - {name : Name of the view you would like to create} - """ - - scaffold_name = "View" - template = "/masonite/snippets/scaffold/view" - file_extension = ".html" - base_directory = "resources/templates/" diff --git a/src/masonite/commands/__init__.py b/src/masonite/commands/__init__.py index a3d034327..6c1fd110a 100644 --- a/src/masonite/commands/__init__.py +++ b/src/masonite/commands/__init__.py @@ -1,27 +1,15 @@ +from .CommandCapsule import CommandCapsule from .AuthCommand import AuthCommand -from .BaseScaffoldCommand import BaseScaffoldCommand -from .CommandCommand import CommandCommand -from .ControllerCommand import ControllerCommand -from .DownCommand import DownCommand -from .InfoCommand import InfoCommand -from .InstallCommand import InstallCommand -from .JobCommand import JobCommand +from .TinkerCommand import TinkerCommand from .KeyCommand import KeyCommand -from .MailableCommand import MailableCommand -from .MiddlewareCommand import MiddlewareCommand -from .ModelCommand import ModelCommand -from .ModelDocstringCommand import ModelDocstringCommand -from .NewCommand import NewCommand -from .PresetCommand import PresetCommand -from .ProviderCommand import ProviderCommand -from .PublishCommand import PublishCommand +from .ServeCommand import ServeCommand from .QueueWorkCommand import QueueWorkCommand +from .QueueRetryCommand import QueueRetryCommand from .QueueTableCommand import QueueTableCommand -from .ServeCommand import ServeCommand -from .ViewCommand import ViewCommand -from .RoutesCommand import RoutesCommand -from .SeedCommand import SeedCommand -from .SeedRunCommand import SeedRunCommand -from .TestCommand import TestCommand -from .TinkerCommand import TinkerCommand -from .UpCommand import UpCommand +from .QueueFailedCommand import QueueFailedCommand +from .MakeControllerCommand import MakeControllerCommand +from .MakeJobCommand import MakeJobCommand +from .MakeMailableCommand import MakeMailableCommand +from .MakeProviderCommand import MakeProviderCommand +from .PublishPackageCommand import PublishPackageCommand +from .MakePolicyCommand import MakePolicyCommand diff --git a/src/masonite/commands/_devserver.py b/src/masonite/commands/_devserver.py deleted file mode 100644 index ef7430515..000000000 --- a/src/masonite/commands/_devserver.py +++ /dev/null @@ -1,140 +0,0 @@ -# Pure-python development wsgi server. -# Parts are borrowed from Adrian Holovaty and the Django Project -# (https://www.djangoproject.com/). - -import logging -import socket -import sys -from wsgiref import simple_server - -logging.basicConfig(level=logging.INFO) - - -def is_broken_pipe_error(): - exc_type, exc_value = sys.exc_info()[:2] - return issubclass(exc_type, socket.error) and exc_value.args[0] == 32 - - -class WSGIServer(simple_server.WSGIServer): - """BaseHTTPServer that implements the Python WSGI protocol""" - - request_queue_size = 10 - - def __init__(self, *args, ipv6=False, allow_reuse_address=True, **kwargs): - if ipv6: - self.address_family = socket.AF_INET6 - self.allow_reuse_address = allow_reuse_address - super().__init__(*args, **kwargs) - - def handle_error(self, request, client_address): - if is_broken_pipe_error(): - logging.info("- Broken pipe from %s\n", client_address) - else: - super().handle_error(request, client_address) - - -class ServerHandler(simple_server.ServerHandler): - http_version = "1.1" - - def handle_error(self): - # Ignore broken pipe errors, otherwise pass on - if not is_broken_pipe_error(): - super().handle_error() - - -class WSGIRequestHandler(simple_server.WSGIRequestHandler): - protocol_version = "HTTP/1.1" - - def address_string(self): - # Short-circuit parent method to not call socket.getfqdn - return self.client_address[0] - - def log_message(self, message_format, *args): - extra = { - "request": self.request, - "server_time": self.log_date_time_string(), - } - if args[1][0] == "4": - # 0x16 = Handshake, 0x03 = SSL 3.0 or TLS 1.x - if args[0].startswith("\x16\x03"): - extra["status_code"] = 500 - logging.error( - "You're accessing the development server over HTTPS, but " - "it only supports HTTP.\n", - extra=extra, - ) - return - - if args[1].isdigit() and len(args[1]) == 3: - status_code = int(args[1]) - extra["status_code"] = status_code - - if status_code >= 500: - level = logging.error - elif status_code >= 400: - level = logging.warning - else: - level = logging.info - else: - level = logging.info - - level(message_format, *args, extra=extra) - - def get_environ(self): - # Strip all headers with underscores in the name before constructing - # the WSGI environ. This prevents header-spoofing based on ambiguity - # between underscores and dashes both normalized to underscores in WSGI - # env vars. Nginx and Apache 2.4+ both do this as well. - for k in self.headers: - if "_" in k: - del self.headers[k] - - return super().get_environ() - - def handle(self): - """Copy of WSGIRequestHandler.handle() but with different ServerHandler""" - self.raw_requestline = self.rfile.readline(65537) - if len(self.raw_requestline) > 65536: - self.requestline = "" - self.request_version = "" - self.command = "" - self.send_error(414) - return - - if not self.parse_request(): # An error code has been sent, just exit - return - - handler = ServerHandler( - self.rfile, self.wfile, self.get_stderr(), self.get_environ() - ) - handler.request_handler = self # backpointer for logging - handler.run(self.server.get_app()) - - -def _split_module_and_app(moduleapp): - if ":" in moduleapp: - parts = moduleapp.split(":") - return parts[0], parts[1] - return moduleapp, "application" - - -def _import_application(module_name, app_name): - from importlib import import_module - - module = import_module(module_name) - return getattr(module, app_name) - - -def run(host, port, wsgi_handler, ipv6=False, httpd_cls=WSGIServer): - if isinstance(wsgi_handler, str): - module_name, app_name = _split_module_and_app(wsgi_handler) - wsgi_handler = _import_application(module_name, app_name) - - server_address = (host, int(port)) - httpd = httpd_cls(server_address, WSGIRequestHandler, ipv6=ipv6) - httpd.set_app(wsgi_handler) - try: - print("Serving at: http://{}:{}".format(host, port)) - httpd.serve_forever() - except KeyboardInterrupt: - pass diff --git a/src/masonite/commands/presets/Bootstrap.py b/src/masonite/commands/presets/Bootstrap.py deleted file mode 100644 index b061bcf9a..000000000 --- a/src/masonite/commands/presets/Bootstrap.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Bootstrap Preset""" -import os -import shutil -from ..presets import Preset - - -class Bootstrap(Preset): - """Configure the front-end scaffolding for the application to use Bootstrap""" - - def install(self): - """Install the preset""" - self.update_packages() - self.update_sass() - self.remove_node_modules() - - def update_package_array(self, packages={}): - """Updates the packages array to include Bootstrap specific packages""" - packages["bootstrap"] = "^4.0.0" - packages["jquery"] = "^3.2" - packages["popper.js"] = "^1.12" - return packages - - def update_sass(self): - """Copies Bootstrap scss files into application""" - directory = "resources/sass" - if not os.path.exists(os.path.realpath(directory)): - os.makedirs(os.path.realpath(directory)) - shutil.copyfile( - os.path.dirname(__file__) + "/bootstrap-stubs/_variables.scss", - "resources/sass/_variables.scss", - ) - shutil.copyfile( - os.path.dirname(__file__) + "/bootstrap-stubs/app.scss", - "resources/sass/app.scss", - ) diff --git a/src/masonite/commands/presets/Preset.py b/src/masonite/commands/presets/Preset.py deleted file mode 100644 index 2c21e8b16..000000000 --- a/src/masonite/commands/presets/Preset.py +++ /dev/null @@ -1,45 +0,0 @@ -import os -import shutil -import json - - -class Preset: - def ensure_component_directory_exists(self): - """Ensure the component directories we need exist.""" - directory = "resources/js/components" - if not os.path.exists(os.path.realpath(directory)): - os.makedirs(os.path.realpath(directory)) - - def update_packages(self, dev=True): - """Update the "package.json" file.""" - if not os.path.exists(os.path.realpath("package.json")): - return - - configuration_key = "devDependencies" if dev else "dependencies" - - packages = {} - with open(os.path.realpath("package.json"), "r+") as f: - packages = json.load(f) - packages[configuration_key] = self.update_package_array( - packages[configuration_key] if configuration_key in packages else {} - ) - f.seek(0) # Rewind to beginning of file - f.truncate() - f.write(json.dumps(packages, sort_keys=True, indent=4)) - - def remove_node_modules(self): - """Remove the installed Node modules.""" - for filename in ["package-lock.json", "yarn.lock"]: - if os.path.exists(os.path.realpath(filename)): - os.remove(filename) - shutil.rmtree("node_modules", ignore_errors=True) - - def create_scss_file(self): - """Create an empty app.scss file""" - os.makedirs(os.path.realpath("resources/sass")) - with open(os.path.realpath("resources/sass/app.scss"), "w") as f: - f.write("// Add your Sass here\n") - f.write("// For Tailwind CSS\n") - f.write("// @import 'tailwindcss/base';\n") - f.write("// @import 'tailwindcss/components';\n") - f.write("// @import 'tailwindcss/utilities';\n") diff --git a/src/masonite/commands/presets/React.py b/src/masonite/commands/presets/React.py deleted file mode 100644 index 4a794ebdc..000000000 --- a/src/masonite/commands/presets/React.py +++ /dev/null @@ -1,64 +0,0 @@ -"""React Preset""" -import os -import shutil -from ..presets import Preset - - -class React(Preset): - """ - Configure the front-end scaffolding for the application to use React - - Will also remove VueJS as React and Vue are a bit mutally exclusive - """ - - def install(self): - """Install the preset""" - self.ensure_component_directory_exists() - self.update_packages() - self.update_webpack_configuration() - self.update_bootstrapping() - self.update_component() - self.create_scss_file() - self.remove_node_modules() - - def update_package_array(self, packages={}): - """ - Updates the packages array to include React specific packages - but also remove VueJS ones - """ - for package in ["vue", "vue-template-compiler"]: - packages.pop(package, None) - - packages["@babel/preset-react"] = "^7.0.0" - packages["react"] = "^16.2.0" - packages["react-dom"] = "^16.2.0" - return packages - - def update_webpack_configuration(self): - """Copy webpack.mix.js file into application""" - shutil.copyfile( - os.path.dirname(__file__) + "/react-stubs/webpack.mix.js", "webpack.mix.js" - ) - - def update_component(self): - """ - Copy example React component into application - (delete example Vue component if it exists) - """ - vue_component = "resources/js/components/ExampleComponent.vue" - if os.path.exists(os.path.realpath(vue_component)): - os.remove(vue_component) - shutil.copyfile( - os.path.dirname(__file__) + "/react-stubs/Example.js", - "resources/js/components/Example.js", - ) - - def update_bootstrapping(self): - """Copies template app.js and bootstrap.js into application""" - shutil.copyfile( - os.path.dirname(__file__) + "/react-stubs/app.js", "resources/js/app.js" - ) - shutil.copyfile( - os.path.dirname(__file__) + "/shared-stubs/bootstrap.js", - "resources/js/bootstrap.js", - ) diff --git a/src/masonite/commands/presets/Remove.py b/src/masonite/commands/presets/Remove.py deleted file mode 100644 index 61309b6f0..000000000 --- a/src/masonite/commands/presets/Remove.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Remove Preset""" -import os -import shutil -from ..presets import Preset - - -class Remove(Preset): - """Removes any defined Preset""" - - def install(self): - """Install the preset""" - self.update_packages() - self.update_bootstrapping() - self.update_webpack_configuration() - self.remove_node_modules() - if os.path.exists(os.path.realpath("resources/sass/_variables.scss")): - os.remove("resources/sass/_variables.scss") - shutil.rmtree("resources/js/components", ignore_errors=True) - shutil.rmtree("public/css", ignore_errors=True) - shutil.rmtree("public/js", ignore_errors=True) - - def update_package_array(self, packages={}): - """Updates the packages array to remove React, VueJS, and Bootstrap packages""" - packages_to_remove = [ - "bootstrap", - "jquery", - "popper.js", - "vue", - "vue-template-compiler", - "@babel/preset-react", - "react", - "react-dom", - ] - for package in packages_to_remove: - packages.pop(package, None) - - return packages - - def update_webpack_configuration(self): - """Copy webpack.mix.js file into application""" - shutil.copyfile( - os.path.dirname(__file__) + "/remove-stubs/webpack.mix.js", "webpack.mix.js" - ) - - def update_bootstrapping(self): - """Copies template app.js file into application""" - for directory in ["resources/sass", "resources/js"]: - if not os.path.exists(os.path.realpath(directory)): - os.makedirs(os.path.realpath(directory)) - with open("resources/sass/app.scss", "w") as f: - f.write("") - shutil.copyfile( - os.path.dirname(__file__) + "/remove-stubs/app.js", "resources/js/app.js" - ) - shutil.copyfile( - os.path.dirname(__file__) + "/remove-stubs/bootstrap.js", - "resources/js/bootstrap.js", - ) diff --git a/src/masonite/commands/presets/Tailwind.py b/src/masonite/commands/presets/Tailwind.py deleted file mode 100644 index 5df85affa..000000000 --- a/src/masonite/commands/presets/Tailwind.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Tailwind Preset""" -import os -import shutil -from ..presets import Preset - - -class Tailwind(Preset): - """ - Configure the front-end scaffolding for the application to use Tailwind - """ - - def install(self): - """Install the preset""" - self.update_packages() - self.update_webpack_configuration() - self.create_tailwind_config() - self.update_scss_file() - self.update_base_views() - self.remove_node_modules() - - def update_package_array(self, packages={}): - """ - Updates the packages array to include VueJS specific packages - """ - packages["autoprefixer"] = "^10.2.1" - packages["tailwindcss"] = "^2.0.2" - return packages - - def update_webpack_configuration(self): - """Copy webpack.mix.js file into application""" - shutil.copyfile( - os.path.dirname(__file__) + "/tailwind-stubs/webpack.mix.js", - "webpack.mix.js", - ) - - def create_tailwind_config(self): - """ - Copy example Tailwind configuration into application - """ - shutil.copyfile( - os.path.dirname(__file__) + "/tailwind-stubs/tailwind.config.js", - "tailwind.config.js", - ) - - def update_scss_file(self): - """Create a app.scss file configured for Tailwind.""" - shutil.copyfile( - os.path.dirname(__file__) + "/tailwind-stubs/style.scss", - "storage/static/sass/style.scss", - ) - - def update_base_views(self): - """Update base views""" - shutil.copyfile( - os.path.dirname(__file__) + "/tailwind-stubs/base.html", - "resources/templates/base.html", - ) - shutil.copyfile( - os.path.dirname(__file__) + "/tailwind-stubs/welcome.html", - "resources/templates/welcome.html", - ) diff --git a/src/masonite/commands/presets/Vue.py b/src/masonite/commands/presets/Vue.py deleted file mode 100644 index fdf999837..000000000 --- a/src/masonite/commands/presets/Vue.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Vue Preset""" -import os -import shutil -from ..presets import Preset - - -class Vue(Preset): - """ - Configure the front-end scaffolding for the application to use VueJS - - Will also remove React as React and Vue are a bit mutally exclusive - """ - - def install(self): - """Install the preset""" - self.ensure_component_directory_exists() - self.update_packages() - self.update_webpack_configuration() - self.update_bootstrapping() - self.update_component() - self.create_scss_file() - self.remove_node_modules() - - def update_package_array(self, packages={}): - """ - Updates the packages array to include VueJS specific packages - but also remove React ones - """ - for package in ["@babel/preset-react", "react", "react-dom"]: - packages.pop(package, None) - - packages["vue"] = "^2.6.12" - return packages - - def update_webpack_configuration(self): - """Copy webpack.mix.js file into application""" - shutil.copyfile( - os.path.dirname(__file__) + "/vue-stubs/webpack.mix.js", "webpack.mix.js" - ) - - def update_component(self): - """ - Copy example VueJS component into application - (delete example React component if it exists) - """ - vue_component = "resources/js/components/Example.js" - if os.path.exists(os.path.realpath(vue_component)): - os.remove(vue_component) - shutil.copyfile( - os.path.dirname(__file__) + "/vue-stubs/ExampleComponent.vue", - "resources/js/components/ExampleComponent.vue", - ) - - def update_bootstrapping(self): - """Copies template app.js and bootstrap.js into application""" - shutil.copyfile( - os.path.dirname(__file__) + "/vue-stubs/app.js", "resources/js/app.js" - ) - shutil.copyfile( - os.path.dirname(__file__) + "/shared-stubs/bootstrap.js", - "resources/js/bootstrap.js", - ) diff --git a/src/masonite/commands/presets/Vue3.py b/src/masonite/commands/presets/Vue3.py deleted file mode 100644 index a7a0463a1..000000000 --- a/src/masonite/commands/presets/Vue3.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Vue 3 Preset""" -import os -import shutil -from ..presets import Preset - - -class Vue3(Preset): - """ - Configure the front-end scaffolding for the application to use VueJS 3.0 - - Will also remove React as React and Vue are a bit mutally exclusive - """ - - def install(self): - """Install the preset""" - self.ensure_component_directory_exists() - self.update_packages() - self.update_webpack_configuration() - self.update_bootstrapping() - self.update_component() - self.create_scss_file() - self.create_view() - self.remove_node_modules() - - def update_package_array(self, packages={}): - """ - Updates the packages array to include VueJS specific packages - but also remove React ones - """ - for package in ["@babel/preset-react", "react", "react-dom"]: - packages.pop(package, None) - - packages["vue"] = "^3.0.4" - packages["@vue/compiler-sfc"] = "^3.0.4" - packages["vue-loader"] = "^16.1.2" - - return packages - - def update_webpack_configuration(self): - """Copy webpack.mix.js file into application""" - shutil.copyfile( - os.path.dirname(__file__) + "/vue3-stubs/webpack.mix.js", "webpack.mix.js" - ) - - def update_component(self): - """ - Copy example VueJS component into application - (delete example React component if it exists) - """ - # delete React component if exists - vue_component = "resources/js/components/Example.js" - if os.path.exists(os.path.realpath(vue_component)): - os.remove(vue_component) - - shutil.copyfile( - os.path.dirname(__file__) + "/vue3-stubs/HelloWorld.vue", - os.path.join("resources/js/components/", "HelloWorld.vue"), - ) - shutil.copyfile( - os.path.dirname(__file__) + "/vue3-stubs/App.vue", - os.path.join("resources/js/", "App.vue"), - ) - - def update_bootstrapping(self): - """Copies template app.js and bootstrap.js into application""" - shutil.copyfile( - os.path.dirname(__file__) + "/vue3-stubs/app.js", "resources/js/app.js" - ) - shutil.copyfile( - os.path.dirname(__file__) + "/shared-stubs/bootstrap.js", - "resources/js/bootstrap.js", - ) - - def create_view(self): - """Copy an example app view with assets included""" - shutil.copyfile( - os.path.dirname(__file__) + "/vue3-stubs/app.html", - "resources/templates/app_vue3.html", - ) diff --git a/src/masonite/commands/presets/__init__.py b/src/masonite/commands/presets/__init__.py deleted file mode 100644 index 95141ed8e..000000000 --- a/src/masonite/commands/presets/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .Preset import Preset -from .React import React -from .Vue import Vue -from .Tailwind import Tailwind -from .Remove import Remove -from .Bootstrap import Bootstrap diff --git a/src/masonite/commands/presets/bootstrap-stubs/_variables.scss b/src/masonite/commands/presets/bootstrap-stubs/_variables.scss deleted file mode 100644 index 0407ab577..000000000 --- a/src/masonite/commands/presets/bootstrap-stubs/_variables.scss +++ /dev/null @@ -1,19 +0,0 @@ -// Body -$body-bg: #f8fafc; - -// Typography -$font-family-sans-serif: 'Nunito', sans-serif; -$font-size-base: 0.9rem; -$line-height-base: 1.6; - -// Colors -$blue: #3490dc; -$indigo: #6574cd; -$purple: #9561e2; -$pink: #f66d9b; -$red: #e3342f; -$orange: #f6993f; -$yellow: #ffed4a; -$green: #38c172; -$teal: #4dc0b5; -$cyan: #6cb2eb; diff --git a/src/masonite/commands/presets/bootstrap-stubs/app.scss b/src/masonite/commands/presets/bootstrap-stubs/app.scss deleted file mode 100644 index eb9482212..000000000 --- a/src/masonite/commands/presets/bootstrap-stubs/app.scss +++ /dev/null @@ -1,13 +0,0 @@ -// Fonts -@import url('https://fonts.googleapis.com/css?family=Nunito'); - -// Variables -@import 'variables'; - -// Bootstrap -@import '~bootstrap/scss/bootstrap'; - -.navbar { - background-color: #fff; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04); -} diff --git a/src/masonite/commands/presets/react-stubs/Example.js b/src/masonite/commands/presets/react-stubs/Example.js deleted file mode 100644 index f18b589a1..000000000 --- a/src/masonite/commands/presets/react-stubs/Example.js +++ /dev/null @@ -1,22 +0,0 @@ -import React, { Component } from 'react'; -import ReactDOM from 'react-dom'; - -export default class Example extends Component { - render() { - return ( --- ); - } -} -if (document.getElementById('example')) { - ReactDOM.render(------Example Component-I'm an example component!-, document.getElementById('example')); -} \ No newline at end of file diff --git a/src/masonite/commands/presets/react-stubs/app.js b/src/masonite/commands/presets/react-stubs/app.js deleted file mode 100644 index bd05c4fb0..000000000 --- a/src/masonite/commands/presets/react-stubs/app.js +++ /dev/null @@ -1,15 +0,0 @@ -/** - * First we will load all of this project's JavaScript dependencies which - * includes React and other helpers. It's a great starting point while - * building robust, powerful web applications using React + Masonite. - */ - -require('./bootstrap'); - -/** - * Next, we will create a fresh React component instance and attach it to - * the page. Then, you may begin adding components to this application - * or customize the JavaScript scaffolding to fit your unique needs. - */ - -require('./components/Example'); \ No newline at end of file diff --git a/src/masonite/commands/presets/react-stubs/webpack.mix.js b/src/masonite/commands/presets/react-stubs/webpack.mix.js deleted file mode 100644 index d6d6725d4..000000000 --- a/src/masonite/commands/presets/react-stubs/webpack.mix.js +++ /dev/null @@ -1,15 +0,0 @@ -const mix = require('laravel-mix'); - -/* - |-------------------------------------------------------------------------- - | Mix Asset Management - |-------------------------------------------------------------------------- - | - | Mix provides a clean, fluent API for defining some Webpack build steps - | for your Masonite application. By default, we are compiling the Sass - | file for the application as well as bundling up all the JS files. - | - */ - -mix.js('resources/js/app.js', 'public/js').react() - .sass('resources/sass/app.scss', 'public/css'); \ No newline at end of file diff --git a/src/masonite/commands/presets/remove-stubs/app.js b/src/masonite/commands/presets/remove-stubs/app.js deleted file mode 100644 index ab7e693da..000000000 --- a/src/masonite/commands/presets/remove-stubs/app.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * First, we will load all of this project's Javascript utilities and other - * dependencies. Then, we will be ready to develop a robust and powerful - * application frontend using useful Masonite and JavaScript libraries. - */ - -require('./bootstrap'); \ No newline at end of file diff --git a/src/masonite/commands/presets/remove-stubs/bootstrap.js b/src/masonite/commands/presets/remove-stubs/bootstrap.js deleted file mode 100644 index 25620bbdf..000000000 --- a/src/masonite/commands/presets/remove-stubs/bootstrap.js +++ /dev/null @@ -1,25 +0,0 @@ -window._ = require('lodash'); - -/** - * We'll load the axios HTTP library which allows us to easily issue requests - * to our Masonite back-end. This library automatically handles sending the - * CSRF token as a header based on the value of the "XSRF" token cookie. - */ - -window.axios = require('axios'); - -window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; - -/** - * Next we will register the CSRF Token as a common header with Axios so that - * all outgoing HTTP requests automatically have it attached. This is just - * a simple convenience so we don't have to attach every token manually. - */ - -let token = document.head.querySelector('meta[name="csrf-token"]'); - -if (token) { - window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; -} else { - console.error('CSRF token not found: https://docs.masoniteproject.com/security/csrf-protection#ajax-vue-axios'); -} diff --git a/src/masonite/commands/presets/remove-stubs/webpack.mix.js b/src/masonite/commands/presets/remove-stubs/webpack.mix.js deleted file mode 100644 index cd6b2cb4d..000000000 --- a/src/masonite/commands/presets/remove-stubs/webpack.mix.js +++ /dev/null @@ -1,15 +0,0 @@ -const mix = require('laravel-mix'); - -/* - |-------------------------------------------------------------------------- - | Mix Asset Management - |-------------------------------------------------------------------------- - | - | Mix provides a clean, fluent API for defining some Webpack build steps - | for your Masonite application. By default, we are compiling the Sass - | file for the application as well as bundling up all the JS files. - | - */ - -mix.js('resources/js/app.js', 'public/js') - .sass('resources/sass/app.scss', 'public/css'); \ No newline at end of file diff --git a/src/masonite/commands/presets/shared-stubs/bootstrap.js b/src/masonite/commands/presets/shared-stubs/bootstrap.js deleted file mode 100644 index 25620bbdf..000000000 --- a/src/masonite/commands/presets/shared-stubs/bootstrap.js +++ /dev/null @@ -1,25 +0,0 @@ -window._ = require('lodash'); - -/** - * We'll load the axios HTTP library which allows us to easily issue requests - * to our Masonite back-end. This library automatically handles sending the - * CSRF token as a header based on the value of the "XSRF" token cookie. - */ - -window.axios = require('axios'); - -window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; - -/** - * Next we will register the CSRF Token as a common header with Axios so that - * all outgoing HTTP requests automatically have it attached. This is just - * a simple convenience so we don't have to attach every token manually. - */ - -let token = document.head.querySelector('meta[name="csrf-token"]'); - -if (token) { - window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; -} else { - console.error('CSRF token not found: https://docs.masoniteproject.com/security/csrf-protection#ajax-vue-axios'); -} diff --git a/src/masonite/commands/presets/tailwind-stubs/base.html b/src/masonite/commands/presets/tailwind-stubs/base.html deleted file mode 100644 index 929143a34..000000000 --- a/src/masonite/commands/presets/tailwind-stubs/base.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - {% block title %} {{ config('application.name', 'Masonite') }} {% endblock %} - - - - - {% block css %}{% endblock %} - - - --- - - \ No newline at end of file diff --git a/src/masonite/commands/presets/tailwind-stubs/style.scss b/src/masonite/commands/presets/tailwind-stubs/style.scss deleted file mode 100644 index 8fa4b3c5d..000000000 --- a/src/masonite/commands/presets/tailwind-stubs/style.scss +++ /dev/null @@ -1,7 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -.links > a { - @apply uppercase px-6 font-bold text-xs tracking-widest; -} \ No newline at end of file diff --git a/src/masonite/commands/presets/tailwind-stubs/tailwind.config.js b/src/masonite/commands/presets/tailwind-stubs/tailwind.config.js deleted file mode 100644 index d6d3ca2df..000000000 --- a/src/masonite/commands/presets/tailwind-stubs/tailwind.config.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = { - purge: [], - darkMode: false, // or 'media' or 'class' - theme: { - fontFamily: { - "sans": "Raleway, sans-serif", // set as sans-serif font - }, - extend: {}, - }, - variants: { - extend: {}, - }, - plugins: [], -} diff --git a/src/masonite/commands/presets/tailwind-stubs/webpack.mix.js b/src/masonite/commands/presets/tailwind-stubs/webpack.mix.js deleted file mode 100644 index d7ab81c99..000000000 --- a/src/masonite/commands/presets/tailwind-stubs/webpack.mix.js +++ /dev/null @@ -1,22 +0,0 @@ -const mix = require("laravel-mix"); -const path = require("path"); -const tailwindcss = require("tailwindcss") - -/* - |-------------------------------------------------------------------------- - | Mix Asset Management - |-------------------------------------------------------------------------- - | - | Mix provides a clean, fluent API for defining some Webpack build steps - | for your Masonite application. By default, we are compiling the Sass - | file for the application as well as bundling up all the JS files. - | - */ - -mix - .js("storage/static/js/app.js", "storage/compiled/js") - .sass("storage/static/sass/style.scss", "storage/compiled/") - .options({ - processCssUrls: false, - postCss: [tailwindcss("tailwind.config.js")], - }) diff --git a/src/masonite/commands/presets/tailwind-stubs/welcome.html b/src/masonite/commands/presets/tailwind-stubs/welcome.html deleted file mode 100644 index 0514de6a9..000000000 --- a/src/masonite/commands/presets/tailwind-stubs/welcome.html +++ /dev/null @@ -1,39 +0,0 @@ -{% if exists('auth/base') %} - {% extends 'auth/base.html' %} -{% else %} - {% extends 'base.html' %} -{% endif %} - -{% block css %} - - -{% endblock %} - -{% block title %} - Welcome To {{ config('application.name', 'Masonite') }} -{% endblock %} - -{% block content %} -- {% block content %} - - {% endblock %} ----{% endblock %} \ No newline at end of file diff --git a/src/masonite/commands/presets/vue-stubs/ExampleComponent.vue b/src/masonite/commands/presets/vue-stubs/ExampleComponent.vue deleted file mode 100644 index 6b9652464..000000000 --- a/src/masonite/commands/presets/vue-stubs/ExampleComponent.vue +++ /dev/null @@ -1,21 +0,0 @@ - -- {{ config('application.name') }} --- -- -
- - --- - - diff --git a/src/masonite/commands/presets/vue-stubs/app.js b/src/masonite/commands/presets/vue-stubs/app.js deleted file mode 100644 index b224c5b77..000000000 --- a/src/masonite/commands/presets/vue-stubs/app.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * First we will load all of this project's JavaScript dependencies which - * includes Vue and other libraries. It is a great starting point when - * building robust, powerful web applications using Vue and Masonite. - */ - -require("./bootstrap"); - -import Vue from "vue"; - -/** - * The following block of code may be used to automatically register your - * Vue components. It will recursively scan this directory for the Vue - * components and automatically register them with their "basename". - * - * Eg. ./components/ExampleComponent.vue ->------Example Component- -I'm an example component.-- */ - -// const files = require.context('./', true, /\.vue$/i) -// files.keys().map(key => Vue.component(key.split('/').pop().split('.')[0], files(key).default)) - -Vue.component( - "example-component", - require("./components/ExampleComponent.vue").default -); - -/** - * Next, we will create a fresh Vue application instance and attach it to - * the page. Then, you may begin adding components to this application - * or customize the JavaScript scaffolding to fit your unique needs. - */ - -const app = new Vue({ - el: "#app", -}); diff --git a/src/masonite/commands/presets/vue-stubs/webpack.mix.js b/src/masonite/commands/presets/vue-stubs/webpack.mix.js deleted file mode 100644 index c0ff8d71a..000000000 --- a/src/masonite/commands/presets/vue-stubs/webpack.mix.js +++ /dev/null @@ -1,17 +0,0 @@ -const mix = require("laravel-mix"); - -/* - |-------------------------------------------------------------------------- - | Mix Asset Management - |-------------------------------------------------------------------------- - | - | Mix provides a clean, fluent API for defining some Webpack build steps - | for your Masonite application. By default, we are compiling the Sass - | file for the application as well as bundling up all the JS files. - | - */ - -mix - .js("resources/js/app.js", "public/js") - .vue({ version: 2 }) - .sass("resources/sass/app.scss", "public/css"); diff --git a/src/masonite/commands/presets/vue3-stubs/App.vue b/src/masonite/commands/presets/vue3-stubs/App.vue deleted file mode 100644 index 9a646ed6f..000000000 --- a/src/masonite/commands/presets/vue3-stubs/App.vue +++ /dev/null @@ -1,26 +0,0 @@ - - -
- - - - - diff --git a/src/masonite/commands/presets/vue3-stubs/HelloWorld.vue b/src/masonite/commands/presets/vue3-stubs/HelloWorld.vue deleted file mode 100644 index ae0fc9408..000000000 --- a/src/masonite/commands/presets/vue3-stubs/HelloWorld.vue +++ /dev/null @@ -1,35 +0,0 @@ - - {{ msg }}
-Count is: {{ count }}, double is {{ double }}.
- - - - diff --git a/src/masonite/commands/presets/vue3-stubs/app.html b/src/masonite/commands/presets/vue3-stubs/app.html deleted file mode 100644 index 996e36221..000000000 --- a/src/masonite/commands/presets/vue3-stubs/app.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - -- {% block title %}{{ config('application.name', 'Masonite') }}{% endblock%} - - {% block css %}{% endblock %} - - - - - - - diff --git a/src/masonite/commands/presets/vue3-stubs/app.js b/src/masonite/commands/presets/vue3-stubs/app.js deleted file mode 100644 index e53bf97b2..000000000 --- a/src/masonite/commands/presets/vue3-stubs/app.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * First we will load all of this project's JavaScript dependencies which - * includes Vue and other libraries. It is a great starting point when - * building robust, powerful web applications using Vue and Masonite. - */ - -require("./bootstrap"); - -/** - * Next, we will create a fresh Vue application instance - */ -import { createApp } from "vue/dist/vue.esm-bundler.js"; -import App from "./App.vue"; -const app = createApp(App); - -/** You can also create an application without the App.vue - -const app = createApp({}); - -*/ - -/** - * The following block of code may be used to automatically register your - * Vue components. It will recursively scan this directory for the Vue - * components and automatically register them with their "basename". - * - * Eg. ./components/ExampleComponent.vue ->- -const files = require.context("./", true, /\.vue$/i); -files - .keys() - .map((key) => - app.component(key.split("/").pop().split(".")[0], files(key).default) - ); - */ - -/** -Or you can register components manually - -import ExampleComponent from './components/ExampleComponent.vue' -app.component("example-component", ExampleComponent) - - */ - -/** Finally we attach the Vue instance to the page. - * Then, you may begin adding components to this application - * or customize the JavaScript scaffolding to fit your unique needs. - */ - -app.mount("#app"); diff --git a/src/masonite/commands/presets/vue3-stubs/webpack.mix.js b/src/masonite/commands/presets/vue3-stubs/webpack.mix.js deleted file mode 100644 index f5b2affc4..000000000 --- a/src/masonite/commands/presets/vue3-stubs/webpack.mix.js +++ /dev/null @@ -1,31 +0,0 @@ -const mix = require("laravel-mix"); -const path = require("path"); - -// For Tailwind CSS -// const tailwindcss = require("tailwindcss") -/* - |-------------------------------------------------------------------------- - | Mix Asset Management - |-------------------------------------------------------------------------- - | - | Mix provides a clean, fluent API for defining some Webpack build steps - | for your Masonite application. By default, we are compiling the Sass - | file for the application as well as bundling up all the JS files. - | - */ - -mix - .js("resources/js/app.js", "storage/compiled/js") - .vue({ version: 3 }) - .sass("resources/sass/app.scss", "storage/compiled/css"); - -// For Tailwind CSS, append -// .options({ -// processCssUrls: false, -// postCss: [ tailwindcss('tailwind.config.js') ], -// }) - -// New Alias plugin -mix.alias({ - "@": path.resolve("resources/js"), -}); diff --git a/src/masonite/configuration/Configuration.py b/src/masonite/configuration/Configuration.py new file mode 100644 index 000000000..a476f791d --- /dev/null +++ b/src/masonite/configuration/Configuration.py @@ -0,0 +1,74 @@ +from ..facades import Loader +from ..utils.structures import data +from ..exceptions import InvalidConfigurationLocation, InvalidConfigurationSetup + + +class Configuration: + # Foundation configuration keys + reserved_keys = [ + "application", + "auth", + "broadcast", + "cache", + "database", + "filesystem", + "mail", + "notification", + "providers", + "queue", + "session", + ] + + def __init__(self, application): + self.application = application + self._config = data() + + def load(self): + """At boot load configuration from all files and store them in here.""" + config_root = self.application.make("config.location") + for module_name, module in Loader.get_modules( + config_root, raise_exception=True + ).items(): + params = Loader.get_parameters(module) + for name, value in params.items(): + self._config[f"{module_name}.{name.lower()}"] = value + + # check loaded configuration + if not self._config.get("application"): + raise InvalidConfigurationLocation( + f"Config directory {config_root} does not contain required configuration files." + ) + + def merge_with(self, path, external_config): + """Merge external config at key with project config at same key. It's especially + useful in Masonite packages in order to merge the configuration default package with + the package configuration which can be published in project. + + This functions disallow merging configuration using foundation configuration keys + (such as 'application'). + """ + if path in self.reserved_keys: + raise InvalidConfigurationSetup( + f"{path} is a reserved configuration key name. Please use an other key." + ) + if isinstance(external_config, str): + # config is a path and should be loaded + params = Loader.get_parameters(external_config) + else: + params = external_config + base_config = {name.lower(): value for name, value in params.items()} + merged_config = {**base_config, **self.get(path, {})} + self.set(path, merged_config) + + def set(self, path, value): + self._config[path] = value + + def get(self, path, default=None): + try: + config_at_path = self._config[path] + if isinstance(config_at_path, dict): + return data(config_at_path) + else: + return config_at_path + except KeyError: + return default diff --git a/src/masonite/configuration/__init__.py b/src/masonite/configuration/__init__.py new file mode 100644 index 000000000..2847ba4f2 --- /dev/null +++ b/src/masonite/configuration/__init__.py @@ -0,0 +1,2 @@ +from .helpers import config +from .Configuration import Configuration diff --git a/src/masonite/configuration/helpers.py b/src/masonite/configuration/helpers.py new file mode 100644 index 000000000..1e8f0d5af --- /dev/null +++ b/src/masonite/configuration/helpers.py @@ -0,0 +1,5 @@ +from ..facades import Config + + +def config(key, default=None): + return Config.get(key, default) diff --git a/src/masonite/configuration/providers/ConfigurationProvider.py b/src/masonite/configuration/providers/ConfigurationProvider.py new file mode 100644 index 000000000..52c54aaa9 --- /dev/null +++ b/src/masonite/configuration/providers/ConfigurationProvider.py @@ -0,0 +1,16 @@ +from ...providers import Provider + +from ..Configuration import Configuration + + +class ConfigurationProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + config = Configuration(self.application) + config.load() + self.application.bind("config", config) + + def boot(self): + pass diff --git a/src/masonite/configuration/providers/__init__.py b/src/masonite/configuration/providers/__init__.py new file mode 100644 index 000000000..6e0364154 --- /dev/null +++ b/src/masonite/configuration/providers/__init__.py @@ -0,0 +1 @@ +from .ConfigurationProvider import ConfigurationProvider diff --git a/src/masonite/container/__init__.py b/src/masonite/container/__init__.py new file mode 100644 index 000000000..0aca8bbb1 --- /dev/null +++ b/src/masonite/container/__init__.py @@ -0,0 +1 @@ +from .container import Container diff --git a/src/masonite/app.py b/src/masonite/container/container.py similarity index 89% rename from src/masonite/app.py rename to src/masonite/container/container.py index ca72ebe2c..7d1525e16 100644 --- a/src/masonite/app.py +++ b/src/masonite/container/container.py @@ -2,36 +2,32 @@ import inspect -from .exceptions import ( +from ..exceptions import ( ContainerError, MissingContainerBindingNotFound, StrictContainerException, ) -class App: +class Container: """Core of the Service Container. Performs bindings and resolving of objects to and from the container. """ - def __init__( - self, strict=False, override=True, resolve_parameters=False, remember=False - ): - """App class constructor.""" - self.providers = {} - self.strict = strict - self.override = override - self.resolve_parameters = resolve_parameters - self.remember = remember - self._hooks = { - "make": {}, - "bind": {}, - "resolve": {}, - } - - self.swaps = {} - self._remembered = {} + objects = {} + strict = False + override = True + resolve_parameters = {} + remember = False + _hooks = { + "make": {}, + "bind": {}, + "resolve": {}, + } + + swaps = {} + _remembered = {} def bind(self, name, class_obj): """Bind classes into the container with a key value pair. @@ -49,14 +45,14 @@ def bind(self, name, class_obj): class_obj, name ) ) - if self.strict and name in self.providers: + if self.strict and name in self.objects: raise StrictContainerException( "You cannot override a key inside a strict container" ) - if self.override or name not in self.providers: + if self.override or name not in self.objects: self.fire_hook("bind", name, class_obj) - self.providers.update({name: class_obj}) + self.objects.update({name: class_obj}) return self @@ -91,8 +87,8 @@ def make(self, name, *arguments): object -- Returns the object that is fetched. """ - if name in self.providers: - obj = self.providers[name] + if name in self.objects: + obj = self.objects[name] self.fire_hook("make", name, obj) if inspect.isclass(obj): obj = self.resolve(obj, *arguments) @@ -120,7 +116,7 @@ def has(self, name): bool """ if isinstance(name, str): - return name in self.providers + return name in self.objects else: try: self._find_obj(name) @@ -215,21 +211,16 @@ def resolve(self, obj, *resolving_arguments): "This container is not set to resolve parameters. You can set this in the container" " constructor using the 'resolve_parameters=True' keyword argument." ) - try: - if self.remember: - if not inspect.ismethod(obj): - self._remembered[obj] = objects - else: - signature = "{}.{}.{}".format( - obj.__module__, obj.__self__.__class__.__name__, obj.__name__ - ) - self._remembered[signature] = objects - return obj(*objects) - except (TypeError,) as e: - exception = ContainerError - exception.from_obj = obj - raise exception(str(e) + " while calling {}".format(obj)) from e + if self.remember: + if not inspect.ismethod(obj): + self._remembered[obj] = objects + else: + signature = "{}.{}.{}".format( + obj.__module__, obj.__self__.__class__.__name__, obj.__name__ + ) + self._remembered[signature] = objects + return obj(*objects) def collect(self, search): """Fetch a dictionary of objects using a search query. @@ -249,7 +240,7 @@ def collect(self, search): # '*ExceptionHook' # 'Sentry*' # 'Sentry*Hook' - for key, value in self.providers.items(): + for key, value in self.objects.items(): if isinstance(key, str): if search.startswith("*"): if key.endswith(search.split("*")[1]): @@ -268,7 +259,7 @@ def collect(self, search): "There is no '*' in your collection search" ) else: - for provider_key, provider_class in self.providers.items(): + for provider_key, provider_class in self.objects.items(): if ( inspect.isclass(provider_class) and issubclass(provider_class, search) @@ -295,7 +286,7 @@ def _find_annotated_parameter(self, parameter): return self.swaps[parameter.annotation](parameter.annotation, self) return obj - for _, provider_class in self.providers.items(): + for _, provider_class in self.objects.items(): if ( parameter.annotation == provider_class @@ -337,8 +328,8 @@ def _find_parameter(self, keyword): """ parameter = str(keyword) - if parameter != "self" and parameter in self.providers: - obj = self.providers[parameter] + if parameter != "self" and parameter in self.objects: + obj = self.objects[parameter] self.fire_hook("resolve", parameter, obj) return obj elif "=" in parameter: @@ -442,7 +433,7 @@ def _find_obj(self, obj): Returns: object -- Returns the object in the container """ - for _, provider_class in self.providers.items(): + for _, provider_class in self.objects.items(): if obj in (provider_class, provider_class.__class__): return_obj = provider_class self.fire_hook("resolve", obj, return_obj) diff --git a/src/masonite/contracts/AuthContract.py b/src/masonite/contracts/AuthContract.py deleted file mode 100644 index 8546b13cd..000000000 --- a/src/masonite/contracts/AuthContract.py +++ /dev/null @@ -1,15 +0,0 @@ -from abc import ABC as Contract, abstractmethod - - -class AuthContract(Contract): - @abstractmethod - def user(self): - pass - - @abstractmethod - def save(self): - pass - - @abstractmethod - def delete(self): - pass diff --git a/src/masonite/contracts/BroadcastContract.py b/src/masonite/contracts/BroadcastContract.py deleted file mode 100644 index 82118974f..000000000 --- a/src/masonite/contracts/BroadcastContract.py +++ /dev/null @@ -1,11 +0,0 @@ -from abc import ABC, abstractmethod - - -class BroadcastContract(ABC): - @abstractmethod - def ssl(self): - pass - - @abstractmethod - def channel(self): - pass diff --git a/src/masonite/contracts/CacheContract.py b/src/masonite/contracts/CacheContract.py deleted file mode 100644 index 3a3fb45c6..000000000 --- a/src/masonite/contracts/CacheContract.py +++ /dev/null @@ -1,23 +0,0 @@ -from abc import ABC, abstractmethod - - -class CacheContract(ABC): - @abstractmethod - def store(self): - pass - - @abstractmethod - def store_for(self): - pass - - @abstractmethod - def get(self): - pass - - @abstractmethod - def is_valid(self): - pass - - @abstractmethod - def exists(self): - pass diff --git a/src/masonite/contracts/MailContract.py b/src/masonite/contracts/MailContract.py deleted file mode 100644 index ca15657d1..000000000 --- a/src/masonite/contracts/MailContract.py +++ /dev/null @@ -1,19 +0,0 @@ -from abc import ABC, abstractmethod - - -class MailContract(ABC): - @abstractmethod - def to(self): - pass - - @abstractmethod - def template(self): - pass - - @abstractmethod - def send_from(self): - pass - - @abstractmethod - def subject(self): - pass diff --git a/src/masonite/contracts/QueueContract.py b/src/masonite/contracts/QueueContract.py deleted file mode 100644 index 1e940911f..000000000 --- a/src/masonite/contracts/QueueContract.py +++ /dev/null @@ -1,27 +0,0 @@ -from abc import ABC, abstractmethod - - -class QueueContract(ABC): - @abstractmethod - def push(self, *objects, args=(), callback="handle", ran=1, channel=None): - pass - - @abstractmethod - def connect(self): - pass - - @abstractmethod - def consume(self, channel, fair=False): - pass - - @abstractmethod - def work(self): - pass - - @abstractmethod - def run_failed_jobs(self): - pass - - @abstractmethod - def add_to_failed_queue_table(self): - pass diff --git a/src/masonite/contracts/SessionContract.py b/src/masonite/contracts/SessionContract.py deleted file mode 100644 index 0e6510b84..000000000 --- a/src/masonite/contracts/SessionContract.py +++ /dev/null @@ -1,35 +0,0 @@ -from abc import ABC, abstractmethod - - -class SessionContract(ABC): - @abstractmethod - def get(self): - pass - - @abstractmethod - def set(self): - pass - - @abstractmethod - def has(self): - pass - - @abstractmethod - def all(self): - pass - - @abstractmethod - def delete(self): - pass - - @abstractmethod - def flash(self): - pass - - @abstractmethod - def reset(self): - pass - - @abstractmethod - def helper(self): - pass diff --git a/src/masonite/contracts/StorageContract.py b/src/masonite/contracts/StorageContract.py deleted file mode 100644 index b8a236970..000000000 --- a/src/masonite/contracts/StorageContract.py +++ /dev/null @@ -1,140 +0,0 @@ -from abc import ABC as Contract -from abc import abstractmethod - - -class StorageContract(Contract): - @abstractmethod - def put(self, location, contents): - """Puts a file into the correct directory - - Arguments: - location {string} -- The location of the file - contents {string|object|file-like object} -- The file object to add. - """ - pass - - @abstractmethod - def get(self, location): - """Get the file contents - - Arguments: - location {string} -- The location of the file - """ - pass - - @abstractmethod - def append(self, location, contents): - """Get the file contents - - Arguments: - location {string} -- The location of the file. - contents {string|object|file-like object} -- The file object to add. - """ - pass - - @abstractmethod - def delete(self, location): - """Deletes the file. - - Arguments: - location {string} -- The location of the file. - """ - pass - - @abstractmethod - def exists(self, location): - """Checks if a file exists. - - Arguments: - location {string} -- The location of the file. - """ - pass - - @abstractmethod - def driver(self): - pass - - @abstractmethod - def url(self, location): - """Gets the full URL of the file to be served. - - Arguments: - location {string} -- The location of the file. - """ - pass - - @abstractmethod - def size(self, location): - """Gets the size of the file. - - Arguments: - location {string} -- The location of the file. - """ - pass - - @abstractmethod - def extension(self, location): - """Gets the extension of the file. - - Arguments: - location {string} -- The location of the file. - """ - pass - - @abstractmethod - def upload(self, *args, **kwargs): - """Passes all arguments to the upload version of this storage driver. - - Arguments: - location {string} -- The location of the file. - """ - pass - - @abstractmethod - def all(self, location): - """Gets all files in a specific directory - - Arguments: - location {string} -- The location of the directory. - """ - pass - - @abstractmethod - def make_directory(self, directory): - """Make an empty directory - - Arguments: - directory {string} -- The location of the directory. - """ - pass - - @abstractmethod - def delete_directory(self, directory, force=False): - """Delete a directory. - - Arguments: - directory {string} -- The location of the directory - - Keyword Arguments: - force {bool} -- Whether or not a directory with contents should be deleted. (default: {False}) - """ - pass - - @abstractmethod - def move(self, old, new): - """Move a file from 1 location to another. - - Arguments: - old {string} -- The file of the file object to be moved. - new {string} -- The location where the file object should be moved to. - """ - pass - - @abstractmethod - def name(self, location): - """Gets the name of the file with the extension - - Arguments: - location {string} -- The location of the file - """ - pass diff --git a/src/masonite/contracts/UploadContract.py b/src/masonite/contracts/UploadContract.py deleted file mode 100644 index 20efc0a1a..000000000 --- a/src/masonite/contracts/UploadContract.py +++ /dev/null @@ -1,15 +0,0 @@ -from abc import ABC, abstractmethod - - -class UploadContract(ABC): - @abstractmethod - def accept(self): - pass - - @abstractmethod - def validate_extension(self): - pass - - @abstractmethod - def store(self): - pass diff --git a/src/masonite/contracts/__init__.py b/src/masonite/contracts/__init__.py deleted file mode 100644 index 6d05f5e9e..000000000 --- a/src/masonite/contracts/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from .AuthContract import AuthContract -from .BroadcastContract import BroadcastContract -from .CacheContract import CacheContract -from .MailContract import MailContract -from .QueueContract import QueueContract -from .SessionContract import SessionContract -from .StorageContract import StorageContract -from .UploadContract import UploadContract - -from .managers.BroadcastManagerContract import BroadcastManagerContract -from .managers.CacheManagerContract import CacheManagerContract -from .managers.MailManagerContract import MailManagerContract -from .managers.QueueManagerContract import QueueManagerContract -from .managers.SessionManagerContract import SessionManagerContract -from .managers.StorageManagerContract import StorageManagerContract -from .managers.UploadManagerContract import UploadManagerContract diff --git a/src/masonite/contracts/managers/BroadcastManagerContract.py b/src/masonite/contracts/managers/BroadcastManagerContract.py deleted file mode 100644 index 0ffb517a3..000000000 --- a/src/masonite/contracts/managers/BroadcastManagerContract.py +++ /dev/null @@ -1,5 +0,0 @@ -from abc import ABC - - -class BroadcastManagerContract(ABC): - pass diff --git a/src/masonite/contracts/managers/CacheManagerContract.py b/src/masonite/contracts/managers/CacheManagerContract.py deleted file mode 100644 index cea45a79f..000000000 --- a/src/masonite/contracts/managers/CacheManagerContract.py +++ /dev/null @@ -1,5 +0,0 @@ -from abc import ABC - - -class CacheManagerContract(ABC): - pass diff --git a/src/masonite/contracts/managers/MailManagerContract.py b/src/masonite/contracts/managers/MailManagerContract.py deleted file mode 100644 index 89cc95f3e..000000000 --- a/src/masonite/contracts/managers/MailManagerContract.py +++ /dev/null @@ -1,5 +0,0 @@ -from abc import ABC - - -class MailManagerContract(ABC): - pass diff --git a/src/masonite/contracts/managers/QueueManagerContract.py b/src/masonite/contracts/managers/QueueManagerContract.py deleted file mode 100644 index 73c04a23c..000000000 --- a/src/masonite/contracts/managers/QueueManagerContract.py +++ /dev/null @@ -1,5 +0,0 @@ -from abc import ABC - - -class QueueManagerContract(ABC): - pass diff --git a/src/masonite/contracts/managers/SessionManagerContract.py b/src/masonite/contracts/managers/SessionManagerContract.py deleted file mode 100644 index 67dbb328e..000000000 --- a/src/masonite/contracts/managers/SessionManagerContract.py +++ /dev/null @@ -1,5 +0,0 @@ -from abc import ABC - - -class SessionManagerContract(ABC): - pass diff --git a/src/masonite/contracts/managers/StorageManagerContract.py b/src/masonite/contracts/managers/StorageManagerContract.py deleted file mode 100644 index 90d75de43..000000000 --- a/src/masonite/contracts/managers/StorageManagerContract.py +++ /dev/null @@ -1,5 +0,0 @@ -from abc import ABC - - -class StorageManagerContract(ABC): - pass diff --git a/src/masonite/contracts/managers/UploadManagerContract.py b/src/masonite/contracts/managers/UploadManagerContract.py deleted file mode 100644 index f0620bfb5..000000000 --- a/src/masonite/contracts/managers/UploadManagerContract.py +++ /dev/null @@ -1,5 +0,0 @@ -from abc import ABC - - -class UploadManagerContract(ABC): - pass diff --git a/src/masonite/controllers/RedirectController.py b/src/masonite/controllers/RedirectController.py new file mode 100644 index 000000000..3b1b5c7a9 --- /dev/null +++ b/src/masonite/controllers/RedirectController.py @@ -0,0 +1,10 @@ +from ..response import Response + + +class RedirectController: + def __init__(self, url, status): + self.url = url + self.status = status + + def redirect(self, response: Response): + return response.redirect(self.url, status=self.status) diff --git a/src/masonite/controllers/__init__.py b/src/masonite/controllers/__init__.py index fd9c5d18f..b933ec388 100644 --- a/src/masonite/controllers/__init__.py +++ b/src/masonite/controllers/__init__.py @@ -1 +1,2 @@ from .Controller import Controller +from .RedirectController import RedirectController diff --git a/src/masonite/cookies/Cookie.py b/src/masonite/cookies/Cookie.py index 4ae956192..0b5fe55cd 100644 --- a/src/masonite/cookies/Cookie.py +++ b/src/masonite/cookies/Cookie.py @@ -5,7 +5,7 @@ def __init__( value, expires=None, http_only=True, - path=None, + path="/", timezone=None, secure=False, ): diff --git a/src/masonite/cookies/CookieJar.py b/src/masonite/cookies/CookieJar.py index d6ee5fc47..d2e925a4a 100644 --- a/src/masonite/cookies/CookieJar.py +++ b/src/masonite/cookies/CookieJar.py @@ -1,5 +1,7 @@ +import pendulum from .Cookie import Cookie -from ..helpers import cookie_expire_time + +from ..utils.time import cookie_expire_time class CookieJar: @@ -12,26 +14,37 @@ def add(self, name, value, **options): self.cookies.update({name: Cookie(name, value, **options)}) def all(self): + cookies = self.loaded_cookies + cookies.update(self.cookies) + return cookies + + def all_added(self): return self.cookies def get(self, name): - aggregate = self.loaded_cookies - aggregate.update(self.cookies) + aggregate = self.all() return aggregate.get(name) def exists(self, name): return name in self.cookies or name in self.loaded_cookies + def is_expired(self, name): + cookie = self.get(name) + return cookie.expires < pendulum.now() + def delete(self, name): self.deleted_cookies.update( { name: Cookie( - name, "", expires=cookie_expire_time("2 months"), timezone="GMT" + name, "", expires=cookie_expire_time("expired"), timezone="GMT" ) } ) if name in self.cookies: - return self.cookies.pop(name) + self.cookies.pop(name) + + if name in self.loaded_cookies: + self.loaded_cookies.pop(name) def load_cookie(self, key, value): self.loaded_cookies.update({key: Cookie(key, value)}) @@ -47,13 +60,14 @@ def to_dict(self): def load(self, cookie_string): for compound_value in cookie_string.split("; "): - key, value = compound_value.split("=", 1) - self.load_cookie(key, value) + if "=" in compound_value: + key, value = compound_value.split("=", 1) + self.load_cookie(key, value) return self def render_response(self): cookies = [] - for name, cookie in self.all().items(): + for name, cookie in {**self.deleted_cookies, **self.all_added()}.items(): cookies.append(("Set-Cookie", cookie.render())) return cookies diff --git a/src/masonite/drivers/BaseDriver.py b/src/masonite/drivers/BaseDriver.py deleted file mode 100644 index 80426f4b0..000000000 --- a/src/masonite/drivers/BaseDriver.py +++ /dev/null @@ -1,31 +0,0 @@ -"""The base class that all drivers inherit from.""" - - -class BaseDriver: - """Base driver class.""" - - _manager = None - - def driver(self, driver): - """Return an instance of the driver specified. - - Arguments: - driver {string} -- String representation of the driver to be resolved from the container. - This can be values like "s3" or "disk" - - Returns: - masonite.drivers -- Returns an instance of the driver. - """ - return self._manager.driver(driver) - - def load_manager(self, manager): - """Load the manager into the driver. - - Arguments: - manager {masonite.managers} -- Needs to be a Manager class. - - Returns: - self - """ - self._manager = manager - return self diff --git a/src/masonite/drivers/__init__.py b/src/masonite/drivers/__init__.py deleted file mode 100644 index 8f14e1b17..000000000 --- a/src/masonite/drivers/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from .BaseDriver import BaseDriver -from .mail.BaseMailDriver import BaseMailDriver -from .mail.Mailable import Mailable -from .authentication.AuthCookieDriver import AuthCookieDriver -from .authentication.AuthJwtDriver import AuthJwtDriver -from .upload.BaseUploadDriver import BaseUploadDriver -from .queue.BaseQueueDriver import BaseQueueDriver -from .cache.BaseCacheDriver import BaseCacheDriver -from .broadcast.BroadcastAblyDriver import BroadcastAblyDriver -from .broadcast.BroadcastPusherDriver import BroadcastPusherDriver -from .broadcast.BroadcastPubNubDriver import BroadcastPubNubDriver -from .cache.CacheDiskDriver import CacheDiskDriver -from .cache.CacheRedisDriver import CacheRedisDriver -from .mail.MailMailgunDriver import MailMailgunDriver -from .mail.MailSmtpDriver import MailSmtpDriver -from .mail.MailLogDriver import MailLogDriver -from .mail.MailTerminalDriver import MailTerminalDriver -from .queue.QueueAsyncDriver import QueueAsyncDriver -from .queue.QueueAmqpDriver import QueueAmqpDriver -from .queue.QueueDatabaseDriver import QueueDatabaseDriver -from .session.SessionCookieDriver import SessionCookieDriver -from .session.SessionMemoryDriver import SessionMemoryDriver -from .storage.StorageDiskDriver import StorageDiskDriver -from .upload.UploadDiskDriver import UploadDiskDriver -from .upload.UploadS3Driver import UploadS3Driver diff --git a/src/masonite/drivers/authentication/AuthCookieDriver.py b/src/masonite/drivers/authentication/AuthCookieDriver.py deleted file mode 100644 index 961d365cd..000000000 --- a/src/masonite/drivers/authentication/AuthCookieDriver.py +++ /dev/null @@ -1,64 +0,0 @@ -"""AuthCookieDriver Module.""" - -from ...contracts import AuthContract -from ...drivers import BaseDriver -from ...app import App - - -class AuthCookieDriver(BaseDriver, AuthContract): - def __init__(self, app: App): - """AuthCookieDriver initializer. - - Arguments: - request {masonite.request.Request} -- The Masonite request class. - """ - self.app = app - - def user(self, auth_model): - """Gets the user based on this driver implementation - - Arguments: - auth_model {orator.orm.Model} -- An Orator ORM type object. - - Returns: - Model|bool - """ - if self.app.make("Request").get_cookie("token") and auth_model: - return ( - auth_model.where( - "remember_token", self.app.make("Request").get_cookie("token") - ).first() - or False - ) - - return False - - def save(self, remember_token, **_): - """Saves the cookie to some state. - - In this case the state is saving to a cookie. - - Arguments: - remember_token {string} -- A token containing the state. - - Returns: - bool - """ - return self.app.make("Request").cookie("token", remember_token) - - def delete(self): - """Deletes the state depending on the implementation of this driver. - - Returns: - bool - """ - return self.app.make("Request").delete_cookie("token") - - def logout(self): - """Deletes the state depending on the implementation of this driver. - - Returns: - bool - """ - self.delete() - self.app.make("Request").reset_user() diff --git a/src/masonite/drivers/authentication/AuthJwtDriver.py b/src/masonite/drivers/authentication/AuthJwtDriver.py deleted file mode 100644 index 5bd3dacd8..000000000 --- a/src/masonite/drivers/authentication/AuthJwtDriver.py +++ /dev/null @@ -1,105 +0,0 @@ -"""AuthJWTDriver Module.""" - -import pendulum -from ...auth import Auth -from ...contracts import AuthContract -from ...drivers import BaseDriver -from ...exceptions import DriverLibraryNotFound -from ...helpers import config, cookie_expire_time -from ...request import Request - - -class AuthJwtDriver(BaseDriver, AuthContract): - def __init__(self, request: Request): - """AuthCookieDriver initializer. - - Arguments: - request {masonite.request.Request} -- The Masonite request class. - """ - self.request = request - try: - import jwt - - self.jwt = jwt - except ImportError: - raise DriverLibraryNotFound( - "Please install pyjwt by running 'pip install pyjwt'" - ) - - def user(self, auth_model): - """Gets the user based on this driver implementation - - Arguments: - auth_model {orator.orm.Model} -- An Orator ORM type object. - - Returns: - Model|bool - """ - from config.application import KEY - - if self.request.get_cookie("token"): - - try: - token = self.jwt.decode( - self.request.get_cookie("token"), KEY, algorithms=["HS256"] - ) - except self.jwt.exceptions.DecodeError: - self.delete() - return False - - expired = token["expired"] - token.pop("expired") - if not pendulum.from_format(expired, "ddd, DD MMM YYYY H:mm:ss").is_past(): - auth_model = auth_model() - return auth_model.hydrate(token) - - if config("auth.drivers.jwt.reauthentication", True): - auth_model = Auth(self.request).login_by_id( - token[auth_model.__primary_key__] - ) - else: - auth_model.hydrate(token) - - token.update( - { - "expired": cookie_expire_time( - config("auth.drivers.jwt.lifetime", "5 minutes") - ) - } - ) - self.request.cookie("token", token) - return auth_model - return False - - def save(self, _, **kwargs): - """Saves the state of authentication. - - In this case the state is serializing the user model and saving to a token cookie. - - Arguments: - remember_token {string} -- A token containing the state. - - Returns: - bool - """ - from config.application import KEY - - model = kwargs.get("model", False) - serialized_dictionary = model.serialize() - serialized_dictionary.update({"expired": cookie_expire_time("5 minutes")}) - token = self.jwt.encode(serialized_dictionary, KEY, algorithm="HS256") - if isinstance(token, bytes): - token = bytes(token).decode("utf-8") - self.request.cookie("token", token) - - def delete(self): - """Deletes the state depending on the implementation of this driver. - - Returns: - bool - """ - self.request.delete_cookie("token") - - def logout(self): - self.delete() - self.request.reset_user() diff --git a/src/masonite/drivers/broadcast/BroadcastAblyDriver.py b/src/masonite/drivers/broadcast/BroadcastAblyDriver.py deleted file mode 100644 index 8ec9f5783..000000000 --- a/src/masonite/drivers/broadcast/BroadcastAblyDriver.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Module for using the Ably websocket driver.""" - -from ...contracts import BroadcastContract -from ...drivers import BaseDriver -from ...exceptions import DriverLibraryNotFound -from ...helpers import config - - -class BroadcastAblyDriver(BroadcastContract, BaseDriver): - """Class for the Ably Driver.""" - - def __init__(self): - """Ably driver constructor. - - Arguments: - BroadcastConfig {config.broadcast} -- Broadcast configuration setting. - """ - self.ssl_message = True - - def ssl(self, boolean): - """Set whether to send data with SSL enabled. - - Arguments: - boolean {bool} -- Boolean on whether to set SSL. - - Returns: - self - """ - self.ssl_message = boolean - return self - - def channel(self, channels, message, event="base-event"): - """Specify which channel(s) you want to send information to. - - Arguments: - channels {string|list} -- Can be a string for the channel or a list of strings for the channels. - message {string} -- The message you want to send to the channel(s) - - Keyword Arguments: - event {string} -- The event you want broadcasted along with your data. (default: {'base-event'}) - - Raises: - DriverLibraryNotFound -- Thrown when ably is not installed. - - Returns: - string -- Returns the message sent. - """ - try: - from ably import AblyRest - except ImportError: - raise DriverLibraryNotFound( - 'Could not find the "ably" library. Please pip install this library running "pip install ably"' - ) - - configuration = config("broadcast.drivers.ably") - if not configuration: - raise Exception("Could not find ably broadcast configuration") - - client = AblyRest("{0}".format(configuration["secret"])) - - if isinstance(channels, list): - for channel in channels: - ably_channel = client.channels.get(channel) - ably_channel.publish(event, message) - else: - channel = client.channels.get(channels) - channel.publish(event, message) - - return message diff --git a/src/masonite/drivers/broadcast/BroadcastPubNubDriver.py b/src/masonite/drivers/broadcast/BroadcastPubNubDriver.py deleted file mode 100644 index 3d543425f..000000000 --- a/src/masonite/drivers/broadcast/BroadcastPubNubDriver.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Module for the PubNub websocket driver.""" - -from ...contracts import BroadcastContract -from ...drivers import BaseDriver -from ...exceptions import DriverLibraryNotFound -from ...helpers import config - - -class BroadcastPubNubDriver(BroadcastContract, BaseDriver): - """Class for the PubNub websocket driver.""" - - def __init__(self): - """PubNub driver constructor. - - Arguments: - BroadcastConfig {config.broadcast} -- Broadcast configuration. - """ - self.ssl_message = True - - def ssl(self, boolean): - """Set whether to send data with SSL enabled. - - Arguments: - boolean {bool} -- Boolean on whether to set SSL. - - Returns: - self - """ - self.ssl_message = boolean - return self - - def channel(self, channels, message, event="base-event"): - """Specify which channel(s) you want to send information to. - - Arguments: - channels {string|list} -- Can be a string for the channel or a list of strings for the channels. - message {string} -- The message you want to send to the channel(s) - - Keyword Arguments: - event {string} -- The event you want broadcasted along with your data. (default: {'base-event'}) - - Raises: - DriverLibraryNotFound -- Thrown when pubnub is not installed. - - Returns: - string -- Returns the message sent. - """ - try: - from pubnub.pnconfiguration import PNConfiguration - from pubnub.pubnub import PubNub - except ImportError: - raise DriverLibraryNotFound( - 'Could not find the "pubnub" library. Please pip install this library running "pip install pubnub"' - ) - - configuration = config("broadcast.drivers.pubnub") - pnconfig = PNConfiguration() - pnconfig.publish_key = configuration["publish_key"] - pnconfig.subscribe_key = configuration["subscribe_key"] - pnconfig.secret = configuration["secret"] - pnconfig.ssl = self.ssl_message - pnconfig.uuid = config("application.name") - - pubnub = PubNub(pnconfig) - - if isinstance(message, str): - message = {"message": message} - - if isinstance(channels, str): - channels = [channels] - - for channel in channels: - envelope = pubnub.publish().channel(channel).message(message).sync() - if envelope.status.is_error(): - print( - "PubNub Broadcast: error sending message to channel {0}.".format( - channel - ) - ) - return message diff --git a/src/masonite/drivers/broadcast/BroadcastPusherDriver.py b/src/masonite/drivers/broadcast/BroadcastPusherDriver.py deleted file mode 100644 index 1128b56f9..000000000 --- a/src/masonite/drivers/broadcast/BroadcastPusherDriver.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Module for the Pusher websocket driver.""" - -from ...contracts import BroadcastContract -from ...drivers import BaseDriver -from ...exceptions import DriverLibraryNotFound -from ...helpers import config - - -class BroadcastPusherDriver(BroadcastContract, BaseDriver): - """Class for the Pusher websocket driver.""" - - def __init__(self): - """Pusher driver constructor. - - Arguments: - BroadcastConfig {config.broadcast} -- Broadcast configuration. - """ - self.ssl_message = True - - def ssl(self, boolean): - """Set whether to send data with SSL enabled. - - Arguments: - boolean {bool} -- Boolean on whether to set SSL. - - Returns: - self - """ - self.ssl_message = boolean - return self - - def channel(self, channels, message, event="base-event"): - """Specify which channel(s) you want to send information to. - - Arguments: - channels {string|list} -- Can be a string for the channel or a list of strings for the channels. - message {string} -- The message you want to send to the channel(s) - - Keyword Arguments: - event {string} -- The event you want broadcasted along with your data. (default: {'base-event'}) - - Raises: - DriverLibraryNotFound -- Thrown when pusher is not installed. - - Returns: - string -- Returns the message sent. - """ - try: - import pusher - except ImportError: - raise DriverLibraryNotFound( - 'Could not find the "pusher" library. Please pip install this library running "pip install pusher"' - ) - - configuration = config("broadcast.drivers.pusher") - - pusher_client = pusher.Pusher( - app_id=str(configuration["app_id"]), - key=configuration["client"], - secret=configuration["secret"], - cluster=configuration["cluster"], - ssl=self.ssl_message, - ) - - if isinstance(message, str): - message = {"message": message} - - if isinstance(channels, list): - for channel in channels: - pusher_client.trigger(channel, event, message) - else: - pusher_client.trigger(channels, event, message) - - return message diff --git a/src/masonite/drivers/cache/BaseCacheDriver.py b/src/masonite/drivers/cache/BaseCacheDriver.py deleted file mode 100644 index 2430bdd5c..000000000 --- a/src/masonite/drivers/cache/BaseCacheDriver.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Base cache driver module.""" - -from ...drivers import BaseDriver - - -class BaseCacheDriver(BaseDriver): - """Base class that all cache drivers inherit from.""" - - def calculate_time(self, cache_type, cache_time): - """Convert time to required unit - Returns: - self - """ - - cache_type = cache_type.lower() - calc = 0 - - if cache_type in ("second", "seconds"): - # Set time now for - calc = 1 - elif cache_type in ("minute", "minutes"): - calc = 60 - elif cache_type in ("hour", "hours"): - calc = 60 ** 2 - elif cache_type in ("day", "days"): - calc = 60 ** 3 - elif cache_type in ("month", "months"): - calc = 60 ** 4 - elif cache_type in ("year", "years"): - calc = 60 ** 5 - else: - raise ValueError("{0} is not a valid caching type.".format(cache_type)) - - return cache_time * calc diff --git a/src/masonite/drivers/cache/CacheDiskDriver.py b/src/masonite/drivers/cache/CacheDiskDriver.py deleted file mode 100644 index 508f35766..000000000 --- a/src/masonite/drivers/cache/CacheDiskDriver.py +++ /dev/null @@ -1,182 +0,0 @@ -"""Module for the ache disk driver.""" - -import glob -import os -import time - -from ...contracts import CacheContract -from ...drivers import BaseCacheDriver -from ...helpers import config - - -class CacheDiskDriver(CacheContract, BaseCacheDriver): - """Class for the cache disk driver.""" - - def __init__(self): - """Cache disk driver constructor. - - Arguments: - CacheConfig {config.cache} -- Cache configuration module. - Application {config.application} -- Application configuration module. - """ - self.config = config("cache") - self.appconfig = config("application") - self.cache_forever = None - - def store(self, key, value, extension=".txt", location=None): - """Store content in cache file. - - Arguments: - key {string} -- The key to store the cache file into - value {string} -- The value you want to store in the cache - - Keyword Arguments: - extension {string} -- the extension you want to append to the file (default: {".txt"}) - location {string} -- The path you want to store the cache into. (default: {None}) - - Returns: - string -- Returns the key - """ - self.cache_forever = True - if not location: - location = self.config.DRIVERS["disk"]["location"] - - location += "/" - path = os.path.join(location, key + extension) - if not os.path.exists(path): - self._create_directory(path) - - with open(path, "w") as file: - file.write(value) - - return key - - def store_for( - self, key, value, cache_time, cache_type, extension=".txt", location=None - ): - """Store the cache for a specific amount of time. - - Arguments: - key {string} -- The key to store the cache file into - value {string} -- The value you want to store in the cache - cache_time {int|string} -- The time as a string or an integer (1, 2, 5, 100, etc) - cache_type {string} -- The type of time to store for (minute, minutes, hours, seconds, etc) - - Keyword Arguments: - extension {string} -- the extension you want to append to the file (default: {".txt"}) - location {string} -- The path you want to store the cache into. (default: {None}) - - Raises: - ValueError -- Thrown if an invalid cache type was caught (like houes instead of hours). - - Returns: - string -- Returns the key - """ - self.cache_forever = False - - cache_for_time = self.calculate_time(cache_type, cache_time) - - cache_for_time = cache_for_time + time.time() - - key = self.store(key + ":" + str(cache_for_time), value, extension, location) - - return key - - def get(self, key): - """Get the data from a key in the cache.""" - if not self.is_valid(key): - return None - - cache_path = self.config.DRIVERS["disk"]["location"] + "/" - content = "" - - if self.cache_forever: - glob_path = cache_path + key + "*" - else: - glob_path = cache_path + key + ":*" - - try: - with open(glob.glob(glob_path)[0], "r") as file: - content = file.read() - except IndexError: - pass - - return content - - def delete(self, key): - """Delete file cache.""" - cache_path = self.config.DRIVERS["disk"]["location"] + "/" - if self.cache_forever: - glob_path = cache_path + key + "*" - else: - glob_path = cache_path + key + ":*" - - for template in glob.glob(glob_path): - os.remove(template) - - def update(self, key, value, location=None): - """Update a specific cache by key.""" - if not location: - location = self.config.DRIVERS["disk"]["location"] + "/" - - location = os.path.join(location, key) - cache = glob.glob(location + ":*")[0] - - with open(cache, "w") as file: - file.write(str(value)) - - return key - - def exists(self, key): - """Check if the cache exists.""" - cache_path = self.config.DRIVERS["disk"]["location"] + "/" - if self.cache_forever: - glob_path = cache_path + key + "*" - else: - glob_path = cache_path + key + ":*" - - find_template = glob.glob(glob_path) - if find_template: - return True - return False - - def is_valid(self, key): - """Check if a valid cache.""" - cache_path = self.config.DRIVERS["disk"]["location"] + "/" - if self.cache_forever: - glob_path = cache_path + key + "*" - else: - glob_path = cache_path + key + ":*" - - cache_file = glob.glob(glob_path) - if cache_file: - try: - cache_timestamp = float( - os.path.splitext(cache_file[0])[0].split(":")[1] - ) - except IndexError: - if self.cache_forever: - return True - - return False - - if cache_timestamp > time.time(): - return True - - self.delete(key) - return False - - def _create_directory(self, directory): - """Creates a new dir. - - Arguments: - directory {string} -- name of directory to create. - - Returns: - bool - Returns a boolean if the directory was created. - """ - if not os.path.exists(os.path.dirname(directory)): - # Create the path to the model if it does not exist - os.makedirs(os.path.dirname(directory)) - return True - return False diff --git a/src/masonite/drivers/cache/CacheRedisDriver.py b/src/masonite/drivers/cache/CacheRedisDriver.py deleted file mode 100644 index 537308434..000000000 --- a/src/masonite/drivers/cache/CacheRedisDriver.py +++ /dev/null @@ -1,121 +0,0 @@ -"""Module for the ache disk driver.""" - -import os - -from ...contracts import CacheContract -from ...drivers import BaseCacheDriver -from ...exceptions import DriverLibraryNotFound - - -class CacheRedisDriver(CacheContract, BaseCacheDriver): - """Class for the cache redis driver.""" - - def __init__(self): - """Cache redis driver constructor. - - Arguments: - CacheConfig {config.cache} -- Cache configuration module. - Application {config.application} -- Application configuration module. - """ - from config import application, cache - - self.appconfig = application - self.cache_forever = None - self.app_name = os.getenv("APP_NAME", "masonite") - - config = cache.DRIVERS["redis"] - - try: - import redis - - self.redis = redis - except ImportError: - raise DriverLibraryNotFound( - "Could not find the 'redis' library. Run pip install redis to fix this." - ) - - self.connection = redis.StrictRedis( - host=config["host"], - port=config["port"], - password=config["password"], - decode_responses=True, - ) - - def store(self, key, value): - """Stores content in cache file. - - Arguments: - key {string} -- The key to store the cache file into - value {string} -- The value you want to store in the cache - - Keyword Arguments: - extension {string} -- the extension you want to append to the file (default: {".txt"}) - location {string} -- The path you want to store the cache into. (default: {None}) - - Returns: - string -- Returns the key - """ - - self.cache_forever = True - - self.connection.set("{0}_cache_{1}".format(self.app_name, key), value) - - return key - - def store_for(self, key, value, cache_time, cache_type): - """Store the cache for a specific amount of time. - - Arguments: - key {string} -- The key to store the cache file into - value {string} -- The value you want to store in the cache - cache_time {int|string} -- The time as a string or an integer (1, 2, 5, 100, etc) - cache_type {string} -- The type of time to store for (minute, minutes, hours, seconds, etc) - - Keyword Arguments: - extension {string} -- the extension you want to append to the file (default: {".txt"}) - location {string} -- The path you want to store the cache into. (default: {None}) - - Raises: - ValueError -- Thrown if an invalid cache type was caught (like houes instead of hours). - - Returns: - string -- Returns the key - """ - - self.cache_forever = False - cache_for_time = self.calculate_time(cache_type, cache_time) - - self.connection.set( - "{0}_cache_{1}".format(self.app_name, key), value, ex=cache_for_time - ) - - return key - - def get(self, key): - """Get the data from a key in the cache.""" - return self.connection.get("{0}_cache_{1}".format(self.app_name, key)) - - def delete(self, key): - """Delete file cache.""" - self.connection.delete("{0}_cache_{1}".format(self.app_name, key)) - - def update(self, key, value): - """Updates a specific cache by key.""" - time_to_expire = self.connection.ttl("{0}_cache_{1}".format(self.app_name, key)) - - if time_to_expire > 0: - self.connection.set( - "{0}_cache_{1}".format(self.app_name, key), value, ex=time_to_expire - ) - else: - self.connection.set("{0}_cache_{1}".format(self.app_name, key), value) - - return key - - def exists(self, key): - """Check if the cache exists.""" - return self.connection.exists("{0}_cache_{1}".format(self.app_name, key)) - - def is_valid(self, key): - """Check if a valid cache.""" - return self.exists(key) diff --git a/src/masonite/drivers/mail/BaseMailDriver.py b/src/masonite/drivers/mail/BaseMailDriver.py deleted file mode 100644 index 82d4fcae7..000000000 --- a/src/masonite/drivers/mail/BaseMailDriver.py +++ /dev/null @@ -1,220 +0,0 @@ -"""Base mail driver module.""" - -import copy -import re - - -from ...app import App -from ...helpers import config, deprecated -from ...response import Responsable -from .. import BaseDriver - - -MAIL_FROM_RE = re.compile(r'(?:"?([^"]*)"?\s)?(?:(.+@[^>]+)>?)') - - -class BaseMailDriver(BaseDriver, Responsable): - """Base mail driver class. This class is inherited by all mail drivers.""" - - def __init__(self, app: App): - """Base mail driver constructor. - - Arguments: - app {masonite.app.App} -- The Masonite container class. - view {object} -- This is the masonite.view.View class. - """ - self.config = config("mail") - self.app = app - self.to_addresses = [] - self.message_subject = "Subject" - self.message_reply_to = None - self.from_name = self.config.FROM["name"] - self.from_address = self.config.FROM["address"] - self._queue = False - self.html_content = None - self.text_content = None - self._message = None - - def _get_message_for_send_deprecated(self, message_contents): - """Helper method for backwards compatibility to generate a message from .send() - - Args: - message_contents: String - - Returns: - message - """ - # we used to not override self.message_body, so save it and set it back... - old_text, old_html = self.text_content, self.html_content - self.text_content, self.html_content = None, message_contents - data = self.message() - self.text_content, self.html_content = old_text, old_html - return data - - def message(self): - """Creates a message object for the underlying driver. - - Returns: - message - """ - raise NotImplementedError - - @property - def mail_from_header(self): - return '"{0}" <{1}>'.format(self.from_name, self.from_address) - - @property - def mail_to_header(self): - return ",".join(self.to_addresses) - - def text(self, content): - """Set the text content of the email. - - Arguments: - content {string} -- The email text content. - - Returns: - self - """ - self.text_content = content - return self - - def html(self, content): - """Set the html content of the email. - - Arguments: - content {string} -- The email html content. - - Returns: - self - """ - self.html_content = content - return self - - @property - def message_body(self): - """Returns the body of the message.""" - return self.html_content or self.text_content - - @message_body.setter - @deprecated("Please use `.text()` and `.html()` methods instead.") - def message_body(self, value): - self.html_content = value - - def to(self, user_email): - """Set the user email address who you want to send mail to. - - Arguments: - user_email {string} -- The user email address. - - Returns: - self - """ - if callable(user_email): - user_email = user_email.email - - if isinstance(user_email, (list, tuple)): - self.to_addresses = user_email - else: - self.to_addresses = [user_email] - return self - - def queue(self): - """Whether the email should be queued or not when sending. - - Returns: - self - """ - self._queue = True - return self - - def template(self, template_name, dictionary={}, mimetype="html"): - """Create an email from a normal Jinja template. - - Arguments: - template_name {string} -- The name of the template. - - Keyword Arguments: - dictionary {dict} -- The data to be passed to the template. (default: {{}}) - mimetype {string} -- whether it is html or text content. (default: {html}) - - Returns: - self - """ - view = copy.copy(self.app.make("ViewClass")) - content = view.render(template_name, dictionary).rendered_template - if mimetype == "html": - self.html(content) - else: - self.text(content) - return self - - def send_from(self, address, name=None): - """Set the from address of who the sender should be. - - Arguments: - address {string} -- A email address used as the From field in an email. - "John S" - John S - john@example.com - name {string} -- A name used as the From field in an email. - - Returns: - self - """ - match = MAIL_FROM_RE.match(address) - if not match: - raise ValueError("Invalid address specified") - - match_name, match_address = match.groups() - self.from_address = match_address - if name is None and match_name: - self.from_name = match_name - - return self - - def mailable(self, mailable): - """Set the from address of who the sender should be. - - Arguments: - address {string} -- A name used as the From field in an email. - - Returns: - self - """ - mailable = self.app.resolve(mailable.build) - ( - self.to(mailable._to) - .send_from(mailable._from) - .subject(mailable._subject) - .template(mailable.template, mailable.variables) - .reply_to(mailable._reply_to) - ) - return self - - def subject(self, subject): - """Set the subject of an email. - - Arguments: - subject {string} -- The subject of the email - - Returns: - self - """ - self.message_subject = subject - return self - - def get_response(self): - return self.message_body - - def reply_to(self, reply_to): - """Set the Reply-To of an email. - - Arguments: - reply_to {string} -- The reply-to of the email - - Returns: - self - """ - self.message_reply_to = reply_to - return self diff --git a/src/masonite/drivers/mail/MailLogDriver.py b/src/masonite/drivers/mail/MailLogDriver.py deleted file mode 100644 index b59a7de77..000000000 --- a/src/masonite/drivers/mail/MailLogDriver.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Log Driver Module.""" - -import logging -import os - -from ...app import App -from ...contracts import MailContract -from ...drivers import BaseMailDriver - - -class MailLogDriver(BaseMailDriver, MailContract): - """Mail log driver.""" - - def __init__(self, app: App): - super().__init__(app) - - if "log" in self.config.DRIVERS and "location" in self.config.DRIVERS["log"]: - log_location = self.config.DRIVERS["log"]["location"] - else: - log_location = "bootstrap/mail" - - if not os.path.exists(log_location): - # Create the path to the model if it does not exist - os.makedirs(log_location) - - handler = logging.FileHandler( - "{0}/{1}".format(log_location, os.getenv("MAIL_LOGFILE", "mail.log")), - delay=True, - ) - self.logger = logging.getLogger(__name__) - self.logger.handlers = [] - self.logger.propagate = False - self.logger.addHandler(handler) - self.logger.setLevel(logging.INFO) - - def send(self, message=None): - """Prints the message in a log. - - Keyword Arguments: - message {string} -- The message to be printed. (default: { None }) - - Returns: - None - """ - - if not message: - message = self.message_body - - self.logger.info("***************************************") - - self.logger.info("To: {}".format(self.mail_to_header)) - self.logger.info("From: {}".format(self.mail_from_header)) - self.logger.info("Subject: {}".format(self.message_subject)) - self.logger.info("Reply-To: {}".format(self.message_reply_to)) - self.logger.info("Message: ") - self.logger.info(message) - - self.logger.info("***************************************") diff --git a/src/masonite/drivers/mail/MailMailgunDriver.py b/src/masonite/drivers/mail/MailMailgunDriver.py deleted file mode 100644 index d67b035f2..000000000 --- a/src/masonite/drivers/mail/MailMailgunDriver.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Mailgun Driver Module.""" -import warnings - -import requests - -from ...contracts.MailContract import MailContract -from ...drivers import BaseMailDriver - - -class MailMailgunDriver(BaseMailDriver, MailContract): - """Mailgun driver.""" - - def message(self): - data = { - "from": self.mail_from_header, - "to": self.to_addresses, - "subject": self.message_subject, - "h:Reply-To": self.message_reply_to, - } - - # Attach both mimetypes if they exist. - if self.text_content: - data["text"] = self.text_content - if self.html_content: - data["html"] = self.html_content - - return data - - def send(self, message=None): - """Send the message through the Mailgun service. - - Keyword Arguments: - message {string} -- The message to be sent to Mailgun. (default: {None}) - - Returns: - requests.post -- Returns the response as a requests object. - """ - if message and isinstance(message, str): - warnings.warn( - "Passing message to .send() is deprecated. Please use .text() and .html().", - category=DeprecationWarning, - stacklevel=2, - ) - data = self._get_message_for_send_deprecated(message) - - # The above should be removed once deprecation time period passed. - elif not message: - data = self.message() - else: - data = message - - if self._queue: - from wsgi import container - from ... import Queue - - return container.make(Queue).push(self._send_mail, args=(data,)) - - return self._send_mail(data) - - def _send_mail(self, data): - """Wrapper around sending mail so it can also be used with queues. - - Arguments: - data {dict} -- The data for mailgun post request. - - Returns: - requests.post - """ - - domain = self.config.DRIVERS["mailgun"]["domain"] - secret = self.config.DRIVERS["mailgun"]["secret"] - - return requests.post( - "https://api.mailgun.net/v3/{0}/messages".format(domain), - auth=("api", secret), - data=data, - ) diff --git a/src/masonite/drivers/mail/MailSmtpDriver.py b/src/masonite/drivers/mail/MailSmtpDriver.py deleted file mode 100644 index 6da4e05e4..000000000 --- a/src/masonite/drivers/mail/MailSmtpDriver.py +++ /dev/null @@ -1,111 +0,0 @@ -"""SMTP Driver Module.""" - -import smtplib -import ssl -import warnings -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText - -from ...contracts.MailContract import MailContract -from ...drivers import BaseMailDriver - - -class MailSmtpDriver(BaseMailDriver, MailContract): - """Mail smtp driver.""" - - def message(self): - """Creates a message object for the underlying driver. - - Returns: - email.mime.multipart.MIMEMultipart - """ - message = MIMEMultipart("alternative") - message["Subject"] = self.message_subject - message["From"] = self.mail_from_header - message["To"] = self.mail_to_header - message["Reply-To"] = self.message_reply_to - - # Attach both mimetypes if they exist. - if self.html_content: - message.attach(MIMEText(self.html_content, "html")) - - if self.text_content: - message.attach(MIMEText(self.text_content, "plain")) - - return message - - def send(self, message=None, message_contents=None): - """Send the message through SMTP. - - Keyword Arguments: - message {string} -- The HTML message to be sent to SMTP. (default: {None}) - - Returns: - None - """ - # The old argument name was `message_contents`. users might have used this as keyword argument or as arg. - assert ( - message is None or message_contents is None - ), 'using deprecated argument "message_contents" together with the new arg "message" ??' - message_contents = message or message_contents - if message_contents and isinstance(message_contents, str): - warnings.warn( - "Passing message_contents to .send() is a deprecated. Please use .text() and .html().", - category=DeprecationWarning, - stacklevel=2, - ) - message = self._get_message_for_send_deprecated(message_contents) - - # The above should be removed once deprecation time period passed. - elif not message: - message = self.message() - - self._smtp_connect() - - if self._queue: - from wsgi import container - from ... import Queue - - container.make(Queue).push( - self._send_mail, - args=(self.mail_from_header, self.to_addresses, message), - ) - return - - return self._send_mail(self.mail_from_header, self.to_addresses, message) - - def _smtp_connect(self): - """Sets self.smtp to an instance of `smtplib.SMTP` - and connects using configuration in config.DRIVERS.smtp - Returns: - None - """ - config = self.config.DRIVERS["smtp"] - if "ssl" in config and config["ssl"] is True: - self.smtp = smtplib.SMTP_SSL( - "{0}:{1}".format(config["host"], config["port"]) - ) - else: - self.smtp = smtplib.SMTP("{0}:{1}".format(config["host"], config["port"])) - - # Check if TLS enabled - if "tls" in config and config["tls"] is True: - # Define secure TLS connection - context = ssl.create_default_context() - context.check_hostname = False - - # Check if correct response code for starttls is received from the server - if self.smtp.starttls(context=context)[0] != 220: - raise smtplib.SMTPNotSupportedError( - "Server is using untrusted protocol." - ) - - if config.get("login", True): - self.smtp.login(config["username"], config["password"]) - - def _send_mail(self, *args): - """Wrapper around sending mail so it can also be used for queues.""" - mail_from_header, to_addresses, message = args - response = self.smtp.send_message(message) - self.smtp.quit() - return response diff --git a/src/masonite/drivers/mail/MailTerminalDriver.py b/src/masonite/drivers/mail/MailTerminalDriver.py deleted file mode 100644 index 7645a7cd3..000000000 --- a/src/masonite/drivers/mail/MailTerminalDriver.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Terminal Driver Module.""" - -import logging - -from ...app import App -from ...contracts.MailContract import MailContract -from ...drivers import BaseMailDriver - - -class MailTerminalDriver(BaseMailDriver, MailContract): - """Mail terminal driver.""" - - def __init__(self, app: App): - super().__init__(app) - self.logger = logging.getLogger(__name__) - self.logger.handlers = [] - handler = logging.StreamHandler() - self.logger.setLevel(logging.INFO) - self.logger.addHandler(handler) - self.logger.propagate = False - - def send(self, message=None): - """Prints the message to the terminal. - - Keyword Arguments: - message {string} -- The message to be printed. (default: { None }) - - Returns: - None - """ - - if not message: - message = self.message_body - - self.logger.info("***************************************") - - self.logger.info("To: {}".format(self.mail_to_header)) - self.logger.info("From: {}".format(self.mail_from_header)) - self.logger.info("Subject: {}".format(self.message_subject)) - self.logger.info("Reply-To: {}".format(self.message_reply_to)) - self.logger.info("Message: ") - self.logger.info(message) - - self.logger.info("***************************************") diff --git a/src/masonite/drivers/mail/Mailable.py b/src/masonite/drivers/mail/Mailable.py deleted file mode 100644 index ce76f13d0..000000000 --- a/src/masonite/drivers/mail/Mailable.py +++ /dev/null @@ -1,26 +0,0 @@ -class Mailable: - - _to = "" - _from = "" - _subject = "" - - def view(self, template, variables={}): - self.template = template - self.variables = variables - return self - - def to(self, to): - self._to = to - return self - - def reply_to(self, reply_to): - self._reply_to = reply_to - return self - - def send_from(self, send_from): - self._from = send_from - return self - - def subject(self, subject): - self._subject = subject - return self diff --git a/src/masonite/drivers/queue/AMQPDriver.py b/src/masonite/drivers/queue/AMQPDriver.py new file mode 100644 index 000000000..a5076eae9 --- /dev/null +++ b/src/masonite/drivers/queue/AMQPDriver.py @@ -0,0 +1,185 @@ +import pickle +import pendulum +import inspect +from ...utils.console import HasColoredOutput + + +class AMQPDriver(HasColoredOutput): + def __init__(self, application): + self.application = application + self.connection = None + self.publishing_channel = None + + def set_options(self, options): + self.options = options + return self + + def push(self, *jobs, args=(), **kwargs): + for job in jobs: + payload = { + "obj": job, + "args": args, + "callback": self.options.get("callback", "handle"), + "created": pendulum.now(tz=self.options.get("tz", "UTC")), + } + + try: + self.connect().publish(payload) + except (self.get_connection_exceptions()): + self.connect().publish(payload) + + def get_connection_exceptions(self): + pika = self.get_package_library() + return ( + pika.exceptions.ConnectionClosed, + pika.exceptions.ChannelClosed, + pika.exceptions.ConnectionWrongStateError, + pika.exceptions.ChannelWrongStateError, + ) + + def publish(self, payload): + pika = self.get_package_library() + self.publishing_channel.basic_publish( + exchange="", + routing_key=self.options.get("queue"), + body=pickle.dumps(payload), + properties=pika.BasicProperties( + delivery_mode=2, # make message persistent + ), + ) + self.publishing_channel.close() + self.connection.close() + + def get_package_library(self): + try: + import pika + except ImportError: + raise ModuleNotFoundError( + "Could not find the 'pika' library. Run 'pip install pika' to fix this." + ) + + return pika + + def connect(self): + try: + import pika + except ImportError: + raise ModuleNotFoundError( + "Could not find the 'pika' library. Run 'pip install pika' to fix this." + ) + + self.connection = pika.BlockingConnection( + pika.URLParameters( + "amqp://{}:{}@{}{}/{}".format( + self.options.get("username"), + self.options.get("password"), + self.options.get("host"), + ":" + str(self.options.get("port")) + if self.options.get("port") + else "", + self.options.get("vhost", "%2F"), + ) + ) + ) + + self.publishing_channel = self.connection.channel() + + self.publishing_channel.queue_declare(self.options.get("queue"), durable=True) + + return self + + def consume(self): + self.success( + '[*] Waiting to process jobs on the "{}" queue. To exit press CTRL+C'.format( + self.options.get("queue") + ) + ) + + self.connect() + + self.publishing_channel.basic_qos(prefetch_count=1) + + self.publishing_channel.basic_consume(self.options.get("queue"), self.work) + + try: + self.publishing_channel.start_consuming() + finally: + self.publishing_channel.stop_consuming() + self.publishing_channel.close() + self.connection.close() + + def retry(self): + builder = ( + self.application.make("builder") + .new() + .on(self.options.get("connection")) + .table(self.options.get("failed_table", "failed_jobs")) + ) + + jobs = builder.get() + + if len(jobs) == 0: + self.success("No failed jobs found.") + return + + for job in jobs: + try: + self.connect().publish(pickle.loads(job["payload"])) + except (self.get_connection_exceptions()): + self.connect().publish(pickle.loads(job["payload"])) + + self.success(f"Added {len(jobs)} failed jobs back to the queue") + builder.table(self.options.get("failed_table", "failed_jobs")).where_in( + "id", [x["id"] for x in jobs] + ).delete() + + def work(self, ch, method, _, body): + + job = pickle.loads(body) + obj = job["obj"] + args = job["args"] + callback = job["callback"] + + try: + try: + if inspect.isclass(obj): + obj = self.application.resolve(obj) + + getattr(obj, callback)(*args) + + except AttributeError: + obj(*args) + + self.success( + f"[{method.delivery_tag}][{pendulum.now(tz=self.options.get('tz', 'UTC')).to_datetime_string()}] Job Successfully Processed" + ) + except Exception as e: + self.danger( + f"[{method.delivery_tag}][{pendulum.now(tz=self.options.get('tz', 'UTC')).to_datetime_string()}] Job Failed" + ) + + getattr(obj, "failed")(job, str(e)) + + self.add_to_failed_queue_table( + self.application.make("builder").new(), str(job["obj"]), body, str(e) + ) + + ch.basic_ack(delivery_tag=method.delivery_tag) + + def add_to_failed_queue_table(self, builder, name, payload, exception): + builder.table(self.options.get("failed_table", "failed_jobs")).create( + { + "driver": "amqp", + "queue": self.options.get("queue", "default"), + "name": name, + "connection": self.options.get("connection"), + "created_at": pendulum.now( + tz=self.options.get("tz", "UTC") + ).to_datetime_string(), + "exception": exception, + "payload": payload, + "failed_at": pendulum.now( + tz=self.options.get("tz", "UTC") + ).to_datetime_string(), + } + ) diff --git a/src/masonite/drivers/queue/QueueAsyncDriver.py b/src/masonite/drivers/queue/AsyncDriver.py similarity index 59% rename from src/masonite/drivers/queue/QueueAsyncDriver.py rename to src/masonite/drivers/queue/AsyncDriver.py index 16ede633f..8678bc86d 100644 --- a/src/masonite/drivers/queue/QueueAsyncDriver.py +++ b/src/masonite/drivers/queue/AsyncDriver.py @@ -1,26 +1,57 @@ -"""Async Driver Method.""" - import inspect import os from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed -from ...app import App -from ...contracts import QueueContract -from ...drivers import BaseQueueDriver from ...exceptions import QueueException -from ...helpers import HasColoredCommands, config -class QueueAsyncDriver(BaseQueueDriver, HasColoredCommands, QueueContract): - """Queue Aysnc Driver.""" +class AsyncDriver: + def __init__(self, application): + self.application = application + + def set_options(self, options): + self.options = options + return self - def __init__(self, app: App): - """Queue Async Driver. + def push(self, *jobs, args=(), **kwargs): + """Push objects onto the async stack. Arguments: - Container {masonite.app.App} -- The application container. + objects {*args of objects} - This can be several objects as parameters into this method. + options {**kwargs of options} - Additional options for async driver """ - self.container = app + + # Initialize Extra Options + options = self.options + callback = options.get("callback", "handle") + mode = options.get("mode", "threading") + workers = options.get("workers", None) + + # Set processor to either use threads or processes + processor = self._get_processor(mode=mode, max_workers=workers) + is_blocking = options.get("blocking", False) + + ran = {} + for obj in jobs: + obj = self.application.resolve(obj) if inspect.isclass(obj) else obj + try: + future = processor.submit(getattr(obj, callback), *args, **kwargs) + except AttributeError: + # Could be wanting to call only a method asynchronously + future = processor.submit(obj, *args, **kwargs) + ran.update({future: obj}) + + if is_blocking: + for job in as_completed(ran.keys()): + if job.exception(): + ran[job].failed(ran[job], job.exception()) + print(f"Job Ran: {job}") + + def consume(self, **options): + pass + + def retry(self, **options): + pass def _get_processor(self, mode, max_workers): """Set processor to use either threads or multiprocesses @@ -30,13 +61,10 @@ def _get_processor(self, mode, max_workers): max_workers {int} - number of threads/processes to use """ - # Necessary for Python 3.4, can be removed in 3.5+ if max_workers is None: # Use this number because ThreadPoolExecutor is often # used to overlap I/O instead of CPU work. max_workers = (os.cpu_count() or 1) * 5 - if max_workers <= 0: - raise QueueException("max_workers must be greater than 0") # Determine Mode for Processing if mode == "threading": @@ -46,35 +74,3 @@ def _get_processor(self, mode, max_workers): else: raise QueueException("Queue mode {} not recognized".format(mode)) return processor - - def push(self, *objects, args=(), kwargs={}, **options): - """Push objects onto the async stack. - - Arguments: - objects {*args of objects} - This can be several objects as parameters into this method. - options {**kwargs of options} - Additional options for async driver - """ - - # Initialize Extra Options - callback = options.get("callback", "handle") - mode = options.get("mode", config("queue.drivers.async.mode", "threading")) - workers = options.get("workers", None) - - # Set processor to either use threads or processes - processor = self._get_processor(mode=mode, max_workers=workers) - is_blocking = config("queue.drivers.async.blocking", False) - - ran = [] - for obj in objects: - obj = self.container.resolve(obj) if inspect.isclass(obj) else obj - try: - future = processor.submit(getattr(obj, callback), *args, **kwargs) - except AttributeError: - # Could be wanting to call only a method asyncronously - future = processor.submit(obj, *args, **kwargs) - - ran.append(future) - - if is_blocking: - for job in as_completed(ran): - self.info("Job Ran: {}".format(job)) diff --git a/src/masonite/drivers/queue/BaseQueueDriver.py b/src/masonite/drivers/queue/BaseQueueDriver.py deleted file mode 100644 index 2365eacdb..000000000 --- a/src/masonite/drivers/queue/BaseQueueDriver.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Base queue driver.""" - -import pickle - -import pendulum - -from ...drivers import BaseDriver -from ...helpers import HasColoredCommands - - -class BaseQueueDriver(BaseDriver, HasColoredCommands): - def add_to_failed_queue_table(self, payload, channel=None, driver="amqp"): - from config.database import DB - from config import queue - - schema = DB.get_schema_builder() - - if schema.has_table("failed_jobs"): - DB.get_query_builder().table("failed_jobs").create( - { - "driver": driver, - "queue": channel, - "channel": channel, - "payload": pickle.dumps(payload), - "failed_at": pendulum.now().to_datetime_string(), - } - ) - - def run_failed_jobs(self): - from config.database import DB - - try: - self.success("Attempting to send failed jobs back to the queue ...") - builder = DB.get_query_builder() - jobs = builder.table("failed_jobs").get() - if not jobs: - return self.success("No failed jobs found") - - self.success(f"Found {len(jobs)} jobs") - for job in builder.table("failed_jobs").get(): - payload = pickle.loads(job["payload"]) - - builder.table("failed_jobs").where("payload", job["payload"]).delete() - self.push( - payload["obj"], args=payload["args"], callback=payload["callback"] - ) - self.success("Jobs successfully added back to the queue.") - except Exception as e: - raise e - self.danger("Could not get the failed_jobs table") - - def push(self, *objects, args=(), callback="handle", ran=1, channel=None): - raise NotImplementedError - - def connect(self): - return self - - def consume(self, channel, **options): - raise NotImplementedError( - "The {} driver does not implement consume".format(self.__class__.__name__) - ) - - def work(self): - raise NotImplementedError( - "The {} driver does not implement work".format(self.__class__.__name__) - ) diff --git a/src/masonite/drivers/queue/DatabaseDriver.py b/src/masonite/drivers/queue/DatabaseDriver.py new file mode 100644 index 000000000..0c7abb34f --- /dev/null +++ b/src/masonite/drivers/queue/DatabaseDriver.py @@ -0,0 +1,205 @@ +import pickle +import pendulum +from ...utils.console import HasColoredOutput +from ...utils.time import parse_human_time +import time + + +class DatabaseDriver(HasColoredOutput): + def __init__(self, application): + self.application = application + + def set_options(self, options): + self.options = options + return self + + def push(self, *jobs, args=(), **kwargs): + builder = self.get_builder() + + available_at = parse_human_time(kwargs.get("delay", "now")) + + for job in jobs: + payload = pickle.dumps( + { + "obj": job, + "args": args, + "kwargs": kwargs, + "callback": self.options.get("callback", "handle"), + } + ) + + builder.create( + { + "name": str(job), + "payload": payload, + "available_at": available_at.to_datetime_string(), + "attempts": 0, + "queue": self.options.get("queue", "default"), + } + ) + + def consume(self): + print("Listening for jobs on queue: " + self.options.get("queue", "default")) + builder = self.get_builder() + + while True: + time.sleep(int(self.options.get("poll", 1))) + if self.options.get("verbosity") == "vv": + print("Checking for available jobs .. ") + builder = builder.new().table(self.options.get("table")) + jobs = ( + builder.where("queue", self.options.get("queue", "default")) + .where( + "available_at", + "<=", + pendulum.now(tz=self.options.get("tz", "UTC")).to_datetime_string(), + ) + .limit(10) + .order_by("id") + .get() + ) + + if self.options.get("verbosity") == "vv": + print(f"Found {len(jobs)} job(s) ") + + builder.where_in("id", [x["id"] for x in jobs]).update( + { + "reserved_at": pendulum.now( + tz=self.options.get("tz", "UTC") + ).to_datetime_string() + } + ) + + for job in jobs: + builder.where("id", job["id"]).table(self.options.get("table")).update( + { + "ran_at": pendulum.now( + tz=self.options.get("tz", "UTC") + ).to_datetime_string(), + } + ) + payload = job["payload"] + unserialized = pickle.loads(job["payload"]) + obj = unserialized["obj"] + args = unserialized["args"] + callback = unserialized["callback"] + + try: + try: + getattr(obj, callback)(*args) + + except AttributeError: + obj(*args) + + self.success( + f"[{job['id']}][{pendulum.now(tz=self.options.get('tz', 'UTC')).to_datetime_string()}] Job Successfully Processed" + ) + if self.options.get("verbosity") == "vv": + print(f"Successful. Deleting Job ID: {job['id']}") + builder.where("id", job["id"]).delete() + except Exception as e: # skipcq + self.danger( + f"[{job['id']}][{pendulum.now(tz=self.options.get('tz', 'UTC')).to_datetime_string()}] Job Failed" + ) + + if job["attempts"] + 1 < self.options.get("attempts", 1): + builder.where("id", job["id"]).table( + self.options.get("table") + ).update( + { + "attempts": job["attempts"] + 1, + } + ) + elif job["attempts"] + 1 >= self.options.get( + "attempts", 1 + ) and not self.options.get("failed_table"): + # Delete the jobs + builder.where("id", job["id"]).table( + self.options.get("table") + ).update( + { + "attempts": job["attempts"] + 1, + } + ) + + if hasattr(obj, "failed"): + getattr(obj, "failed")(unserialized, str(e)) + + builder.where("id", job["id"]).table( + self.options.get("table") + ).delete() + elif self.options.get("failed_table"): + self.add_to_failed_queue_table( + builder, job["name"], payload, str(e) + ) + + if hasattr(obj, "failed"): + getattr(obj, "failed")(unserialized, str(e)) + + builder.where("id", job["id"]).table( + self.options.get("table") + ).delete() + else: + builder.where("id", job["id"]).table( + self.options.get("table") + ).update( + { + "attempts": job["attempts"] + 1, + } + ) + + def retry(self): + builder = self.get_builder() + + jobs = ( + builder.table(self.options.get("failed_table")) + .where("queue", self.options.get("queue", "default")) + .get() + ) + + if len(jobs) == 0: + self.success("No failed jobs found.") + return + + for job in jobs: + builder.table("jobs").create( + { + "name": str(job["name"]), + "payload": job["payload"], + "attempts": 0, + "available_at": pendulum.now( + tz=self.options.get("tz", "UTC") + ).to_datetime_string(), + "queue": job["queue"], + } + ) + self.success(f"Added {len(jobs)} failed job(s) back to the queue") + builder.table(self.options.get("failed_table", "failed_jobs")).where_in( + "id", [x["id"] for x in jobs] + ).delete() + + def get_builder(self): + return ( + self.application.make("builder") + .new() + .on(self.options.get("connection")) + .table(self.options.get("table")) + ) + + def add_to_failed_queue_table(self, builder, name, payload, exception): + builder.table(self.options.get("failed_table", "failed_jobs")).create( + { + "driver": "database", + "queue": self.options.get("queue", "default"), + "name": name, + "connection": self.options.get("connection"), + "created_at": pendulum.now( + tz=self.options.get("tz", "UTC") + ).to_datetime_string(), + "exception": exception, + "payload": payload, + "failed_at": pendulum.now( + tz=self.options.get("tz", "UTC") + ).to_datetime_string(), + } + ) diff --git a/src/masonite/drivers/queue/QueueAmqpDriver.py b/src/masonite/drivers/queue/QueueAmqpDriver.py deleted file mode 100644 index 6453b5fe9..000000000 --- a/src/masonite/drivers/queue/QueueAmqpDriver.py +++ /dev/null @@ -1,183 +0,0 @@ -"""Driver for AMQP support""" - -import inspect -import pickle -import time - -import pendulum -from ...contracts import QueueContract -from ...drivers import BaseQueueDriver -from ...exceptions import DriverLibraryNotFound -from ...helpers import HasColoredCommands -from ...queues import Queueable - - -class QueueAmqpDriver(BaseQueueDriver, QueueContract, HasColoredCommands): - def __init__(self): - """Queue AMQP Driver.""" - from config import queue - - self.queue = queue - if "amqp" in self.queue.DRIVERS: - listening_channel = self.queue.DRIVERS["amqp"]["channel"] - else: - listening_channel = "default" - - # Start the connection - self.publishing_channel = listening_channel - self.connect() - - def _publish(self, body): - - self.channel.basic_publish( - exchange="", - routing_key=self.publishing_channel, - body=pickle.dumps(body), - properties=self.pika.BasicProperties( - delivery_mode=2, # make message persistent - ), - ) - self.channel.close() - self.connection.close() - - def push( - self, *objects, args=(), callback="handle", ran=1, channel=None, **options - ): # skipcq PYL-W0613 - """Push objects onto the amqp stack. - - Arguments: - objects {*args of objects} - This can be several objects as parameters into this method. - """ - if channel: - self.publishing_channel = channel - - for obj in objects: - # Publish to the channel for each object - payload = { - "obj": obj, - "args": args, - "callback": callback, - "created": pendulum.now(), - "ran": ran, - } - try: - additional_exceptions = ( - self.pika.exceptions.ConnectionWrongStateError, - self.pika.exceptions.ChannelWrongStateError, - ) - except AttributeError: - additional_exceptions = () - - try: - self._publish(payload) - except ( - ( - self.pika.exceptions.ConnectionClosed, - self.pika.exceptions.ChannelClosed, - ), - additional_exceptions, - ): - self.connect() - self._publish(payload) - - def connect(self): - try: - import pika - - self.pika = pika - except ImportError: - raise DriverLibraryNotFound( - "Could not find the 'pika' library. Run pip install pika to fix this." - ) - - self.connection = pika.BlockingConnection( - pika.URLParameters( - "amqp://{}:{}@{}{}/{}".format( - self.queue.DRIVERS["amqp"]["username"], - self.queue.DRIVERS["amqp"]["password"], - self.queue.DRIVERS["amqp"]["host"], - ":" + str(self.queue.DRIVERS["amqp"]["port"]) - if "port" in self.queue.DRIVERS["amqp"] - and self.queue.DRIVERS["amqp"]["port"] - else "", - self.queue.DRIVERS["amqp"]["vhost"] - if "vhost" in self.queue.DRIVERS["amqp"] - and self.queue.DRIVERS["amqp"]["vhost"] - else "%2F", - ) - ) - ) - - self.channel = self.connection.channel() - - self.channel.queue_declare(self.publishing_channel, durable=True) - - return self - - def consume(self, channel, fair=False, **options): - self.success( - '[*] Waiting to process jobs on the "{}" channel. To exit press CTRL+C'.format( - channel - ) - ) - - self.queue = channel - - if fair: - self.channel.basic_qos(prefetch_count=1) - - self.basic_consume(self.work, channel) - - try: - self.channel.start_consuming() - finally: - self.channel.stop_consuming() - self.channel.close() - self.connection.close() - - def basic_consume(self, callback, queue_name): - try: - self.channel.basic_consume(callback, queue=queue_name) - except TypeError: - self.channel.basic_consume(queue_name, callback) - - def work(self, ch, method, _, body): - from wsgi import container - - job = pickle.loads(body) - obj = job["obj"] - args = job["args"] - callback = job["callback"] - ran = job["ran"] - - try: - try: - if inspect.isclass(obj): - obj = container.resolve(obj) - - getattr(obj, callback)(*args) - - except AttributeError: - obj(*args) - - try: - self.success("[\u2713] Job Successfully Processed") - except UnicodeEncodeError: - self.success("[Y] Job Successfully Processed") - except Exception as e: - self.danger("Job Failed: {}".format(str(e))) - - if not obj.run_again_on_fail: - ch.basic_ack(delivery_tag=method.delivery_tag) - return - - if ran < obj.run_times and isinstance(obj, Queueable): - time.sleep(1) - self.push(obj.__class__, args=args, callback=callback, ran=ran + 1) - else: - if hasattr(obj, "failed"): - getattr(obj, "failed")(job, str(e)) - - self.add_to_failed_queue_table(job, channel=self.queue) - - ch.basic_ack(delivery_tag=method.delivery_tag) diff --git a/src/masonite/drivers/queue/QueueDatabaseDriver.py b/src/masonite/drivers/queue/QueueDatabaseDriver.py deleted file mode 100644 index feb34cf0f..000000000 --- a/src/masonite/drivers/queue/QueueDatabaseDriver.py +++ /dev/null @@ -1,137 +0,0 @@ -"""Async Driver Method.""" - -import inspect -import pickle -import time - -import pendulum -from ...contracts import QueueContract -from ...drivers import BaseQueueDriver -from ...helpers import HasColoredCommands, parse_human_time -from ...queues import Queueable -from .QueueJobsModel import QueueJobsModel - - -class QueueDatabaseDriver(BaseQueueDriver, HasColoredCommands, QueueContract): - """Queue Aysnc Driver.""" - - def __init__(self): - """Queue Async Driver. - - Arguments: - Container {masonite.app.App} -- The application container. - """ - pass - - def connect(self): - return self - - def push(self, *objects, args=(), kwargs={}, **options): - """Push objects onto the async stack. - - Arguments: - objects {*args of objects} - This can be several objects as parameters into this method. - options {**kwargs of options} - Additional options for async driver - """ - - from config.database import DB as schema - from masoniteorm.query import QueryBuilder - - callback = options.get("callback", "handle") - wait = options.get("wait", None) - connection = options.get("connection", "default") - queue = options.get("queue", "default") - - if wait: - wait = parse_human_time(wait).to_datetime_string() - - for job in objects: - if schema.get_schema_builder(connection).has_table("queue_jobs"): - payload = pickle.dumps( - {"obj": job, "args": args, "kwargs": kwargs, "callback": callback} - ) - - schema.get_query_builder(connection).table("queue_jobs").create( - { - "name": str(job), - "serialized": payload, - "created_at": pendulum.now().to_datetime_string(), - "attempts": 0, - "ran_at": None, - "queue": queue, - "available_at": wait, - "reserved_at": None, - } - ) - - def consume(self, channel, **options): # skipcq - from config.database import DB, DATABASES - from wsgi import container - - self.info( - '[*] Waiting to process jobs from the "queue_jobs" table on the "{}" connection. To exit press CTRL + C'.format( - channel - ) - ) - - builder = QueueJobsModel - while True: - jobs = ( - builder.where_null("ran_at") - .where_null("reserved_at") - .where("queue", options.get("queue", "default")) - .where( - lambda q: q.where_null("available_at").or_where( - "available_at", "<=", pendulum.now().to_datetime_string() - ) - ) - .limit(5) - .order_by("id") - .get() - ) - - builder.where_in("id", jobs.pluck("id")).update( - {"reserved_at": pendulum.now().to_datetime_string()} - ) - - if not jobs.count(): - time.sleep(int(options.get("poll")) or 1) - continue - - for job in jobs: - builder.where("id", job["id"]).update( - { - "ran_at": pendulum.now().to_datetime_string(), - } - ) - unserialized = pickle.loads(job["serialized"]) - obj = unserialized["obj"] - args = unserialized["args"] - callback = unserialized["callback"] - - try: - try: - if inspect.isclass(obj): - obj = container.resolve(obj) - - getattr(obj, callback)(*args) - - except AttributeError: - obj(*args) - - try: - self.success("[\u2713] Job Successfully Processed") - except UnicodeEncodeError: - self.success("[Y] Job Successfully Processed") - builder.where("id", job["id"]).delete() - except Exception as e: # skipcq - self.danger("Job Failed: {}".format(str(e))) - - # if not obj.run_again_on_fail: - builder.where("id", job["id"]).delete() - - if hasattr(obj, "failed"): - getattr(obj, "failed")(unserialized, str(e)) - self.add_to_failed_queue_table( - unserialized, channel=channel, driver="database" - ) diff --git a/src/masonite/drivers/queue/QueueJobsModel.py b/src/masonite/drivers/queue/QueueJobsModel.py deleted file mode 100644 index 882944aa6..000000000 --- a/src/masonite/drivers/queue/QueueJobsModel.py +++ /dev/null @@ -1,6 +0,0 @@ -from masoniteorm.models import Model - - -class QueueJobsModel(Model): - __table__ = "queue_jobs" - __timestamps__ = None diff --git a/src/masonite/drivers/queue/__init__.py b/src/masonite/drivers/queue/__init__.py new file mode 100644 index 000000000..165c6b4f8 --- /dev/null +++ b/src/masonite/drivers/queue/__init__.py @@ -0,0 +1,3 @@ +from .DatabaseDriver import DatabaseDriver +from .AsyncDriver import AsyncDriver +from .AMQPDriver import AMQPDriver diff --git a/src/masonite/drivers/session/CookieDriver.py b/src/masonite/drivers/session/CookieDriver.py new file mode 100644 index 000000000..71024897f --- /dev/null +++ b/src/masonite/drivers/session/CookieDriver.py @@ -0,0 +1,58 @@ +"""Session Cookie Module.""" + + +class CookieDriver: + """Cookie Session Driver.""" + + def __init__(self, application): + """Cookie Session Constructor. + + Arguments: + application {dict} -- The application class + """ + self.application = application + + def start(self): + request = self.get_request() + data = {} + flashed = {} + for key, value in request.cookie_jar.to_dict().items(): + if key.startswith("s_"): + data.update({key.replace("s_", ""): value}) + elif key.startswith("f_"): + flashed.update({key.replace("f_", ""): value}) + + return {"data": data, "flashed": flashed} + + def save(self, added=None, deleted=None, flashed=None, deleted_flashed=None): + response = self.get_response() + if added is None: + added = {} + if deleted is None: + deleted = [] + if flashed is None: + flashed = {} + if deleted_flashed is None: + deleted_flashed = [] + + for key, value in added.items(): + response.cookie(f"s_{key}", value) + + for key, value in flashed.items(): + response.cookie(f"f_{key}", value) + + for key in deleted: + response.delete_cookie(f"s_{key}") + + for key in deleted_flashed: + response.delete_cookie(f"f_{key}") + + def get_response(self): + return self.application.make("response") + + def get_request(self): + return self.application.make("request") + + def helper(self): + """Use to create builtin helper function.""" + return self diff --git a/src/masonite/drivers/session/SessionCookieDriver.py b/src/masonite/drivers/session/SessionCookieDriver.py deleted file mode 100644 index 33ed530e5..000000000 --- a/src/masonite/drivers/session/SessionCookieDriver.py +++ /dev/null @@ -1,203 +0,0 @@ -"""Session Cookie Module.""" - -import json -from email import message - -from ...contracts import SessionContract -from ...drivers import BaseDriver -from ...helpers import config -from ...request import Request - - -class SessionCookieDriver(SessionContract, BaseDriver): - """Cookie Session Driver.""" - - def __init__(self, request: Request): - """Cookie Session Constructor. - - Arguments: - Environ {dict} -- The WSGI environment - Request {masonite.request.Request} -- The Request class. - """ - self.request = request - - def get(self, key): - """Get a value from the session. - - Arguments: - key {string} -- The key to get from the session. - - Returns: - string|None - Returns None if a value does not exist. - """ - cookie = self.request.get_cookie("s_{0}".format(key)) - if cookie: - return self._get_serialization_value(cookie) - - cookie = self.request.get_cookie("f_{0}".format(key)) - if cookie: - return self._get_serialization_value(cookie) - - return None - - def get_flashed(self, key): - value = self.get(key) - if value: - self.delete_flash(key) - return value - - return None - - def set(self, key, value): - """Set a vlue in the session. - - Arguments: - key {string} -- The key to set as the session key. - value {string} -- The value to set in the session. - """ - if isinstance(value, dict): - value = json.dumps(value) - - self.request.cookie("s_{0}".format(key), value) - - def has(self, key): - """Check if a key exists in the session. - - Arguments: - key {string} -- The key to check for in the session. - - Returns: - bool - """ - if self.get(key): - return True - return False - - def all(self, flash_only=False): - """Get all session data. - - Returns: - dict - """ - return self.__collect_data(flash_only=flash_only) - - def delete(self, key): - """Delete a value in the session by it's key. - - Arguments: - key {string} -- The key to find in the session. - - Returns: - bool -- If the key was deleted or not - """ - self.__collect_data() - - if self.request.get_cookie("s_{}".format(key)): - self.request.delete_cookie("s_{}".format(key)) - return True - - return False - - def delete_flash(self, key): - """Delete a value in the session by it's key. - - Arguments: - key {string} -- The key to find in the session. - - Returns: - bool -- If the key was deleted or not - """ - self.__collect_data() - - if self.request.get_cookie("f_{}".format(key)): - self.request.delete_cookie("f_{}".format(key)) - return True - - return False - - def __collect_data(self, flash_only=False): - """Collect data from session and flash data. - - Returns: - dict - """ - cookies = {} - all_cookies = self.request.get_cookies().to_dict() - for key, value in all_cookies.items(): - if not (key.startswith("f_") or key.startswith("s_")): - continue - - if flash_only and not key.startswith("f_"): - continue - - key = key.replace("f_", "").replace("s_", "") - - cookies.update({key: self.get(key)}) - return cookies - - return cookies - - def flash(self, key, value): - """Add temporary data to the session. - - Arguments: - key {string} -- The key to set as the session key. - value {string} -- The value to set in the session. - """ - if isinstance(value, (dict, list)): - value = json.dumps(value) - - self.request.cookie( - "f_{0}".format(key), - value, - ) - - def get_error_messages(self): - """Should get and delete the flashed messages - - Arguments: - key {string} -- The key to set as the session key. - value {string} -- The value to set in the session. - """ - only_messages = [] - messages = self.all(flash_only=True).get("errors", {}).items() - for key, messages in messages: - for error_message in messages: - only_messages.append(error_message) - self.reset(flash_only=True) - return only_messages - - def get_flashed_messages(self): - """Should get and delete the flashed messages - - Arguments: - key {string} -- The key to set as the session key. - value {string} -- The value to set in the session. - """ - messages = self.all(flash_only=True) - self.reset(flash_only=True) - return messages - - def reset(self, flash_only=False): - """Delete all session data. - - Keyword Arguments: - flash_only {bool} -- If only flash data should be deleted. (default: {False}) - """ - cookies = self.__collect_data() - for cookie in cookies: - if flash_only: - self.request.delete_cookie("f_{0}".format(cookie)) - continue - - self.request.delete_cookie("s_{0}".format(cookie)) - - def helper(self): - """Use to create builtin helper function.""" - return self - - def _get_serialization_value(self, value): - try: - return json.loads(value) - except ValueError: - return value diff --git a/src/masonite/drivers/session/SessionMemoryDriver.py b/src/masonite/drivers/session/SessionMemoryDriver.py deleted file mode 100644 index 61e88f915..000000000 --- a/src/masonite/drivers/session/SessionMemoryDriver.py +++ /dev/null @@ -1,207 +0,0 @@ -"""Session Memory Module.""" - -from ...contracts import SessionContract -from ...drivers import BaseDriver -from ...request import Request - - -class SessionMemoryDriver(SessionContract, BaseDriver): - """Memory Session Driver.""" - - _session = {} - _flash = {} - - def __init__(self, request: Request): - """Cookie Session Constructor. - - Arguments: - Environ {dict} -- The WSGI environment - """ - self.request = request - - def get(self, key): - """Get a value from the session. - - Arguments: - key {string} -- The key to get from the session. - - Returns: - string|None - Returns None if a value does not exist. - """ - data = self.__collect_data(key) - if data: - return data - - return None - - def set(self, key, value): - """Set a vlue in the session. - - Arguments: - key {string} -- The key to set as the session key. - value {string} -- The value to set in the session. - """ - ip = self.__get_client_address() - - if ip not in self._session: - self._session[ip] = {} - - self._session[ip][key] = value - - def has(self, key): - """Check if a key exists in the session. - - Arguments: - key {string} -- The key to check for in the session. - - Returns: - bool - """ - data = self.__collect_data() - if data and key in data: - return True - return False - - def all(self): - """Get all session data. - - Returns: - dict - """ - return self.__collect_data() - - def flash(self, key, value): - """Add temporary data to the session. - - Arguments: - key {string} -- The key to set as the session key. - value {string} -- The value to set in the session. - """ - ip = self.__get_client_address() - if ip not in self._flash: - self._flash[ip] = {} - - self._flash[ip][key] = value - - def get_flashed(self, key): - value = self.get(key) - if value: - self.delete_flash(key) - return value - - return None - - def get_error_messages(self): - """Should get and delete the flashed messages - - Arguments: - key {string} -- The key to set as the session key. - value {string} -- The value to set in the session. - """ - ip = self.__get_client_address() - only_messages = [] - messages = self._flash.get(ip, {}).get("errors", {}).items() - for key, messages in messages: - for message in messages: - only_messages.append(message) - self.reset(flash_only=True) - return only_messages - - def get_flashed_messages(self): - """Should get and delete the flashed messages - - Arguments: - key {string} -- The key to set as the session key. - value {string} -- The value to set in the session. - """ - messages = self._flash.get(ip, {}) - self.reset(flash_only=True) - return messages - - def reset(self, flash_only=False): - """Delete all session data. - - Keyword Arguments: - flash_only {bool} -- If only flash data should be deleted. (default: {False}) - """ - ip = self.__get_client_address() - - if flash_only: - if ip in self._flash: - self._flash[ip] = {} - else: - if ip in self._session: - self._session[ip] = {} - - def delete(self, key): - """Delete a value in the session by it's key. - - Arguments: - key {string} -- The key to find in the session. - - Returns: - bool -- If the key was deleted or not - """ - data = self.__collect_data() - - if key in data: - del data[key] - return True - - return False - - def delete_flash(self, key): - """Delete a value in the session by it's key. - - Arguments: - key {string} -- The key to find in the session. - - Returns: - bool -- If the key was deleted or not - """ - return self.delete(key) - - def __get_client_address(self): - """Get ip from the client.""" - if "HTTP_X_FORWARDED_FOR" in self.request.environ: - return self.request.environ["HTTP_X_FORWARDED_FOR"].split(",")[-1].strip() - - return self.request.environ["REMOTE_ADDR"] - - def __collect_data(self, key=False): - """Collect data from session and flash data. - - Returns: - dict - """ - ip = self.__get_client_address() - - # Declare a new dictionary - session = {} - - # If the session data has keys - if ip in self._session: - session = self._session[ip] - - # If the session flash has keys - if ip in self._flash: - session.update(self._flash[ip]) - - # If a key is set and it is inside the new declared session, return that key - if key and key in session: - return session[key] - - # If the key is set and is not in the session - if key and key not in session: - return None - - # If the session is still an empty dictionary - if not session: - return None - - # No checks have been hit. Return the new dictionary - return session - - def helper(self): - """Used to create builtin helper function.""" - return self diff --git a/src/masonite/drivers/session/__init__.py b/src/masonite/drivers/session/__init__.py new file mode 100644 index 000000000..234b67c5c --- /dev/null +++ b/src/masonite/drivers/session/__init__.py @@ -0,0 +1 @@ +from .CookieDriver import CookieDriver diff --git a/src/masonite/drivers/storage/StorageDiskDriver.py b/src/masonite/drivers/storage/StorageDiskDriver.py deleted file mode 100644 index 6f2d11ead..000000000 --- a/src/masonite/drivers/storage/StorageDiskDriver.py +++ /dev/null @@ -1,75 +0,0 @@ -import os -import pathlib -import shutil - -from ... import Upload -from ...contracts import StorageContract -from ...drivers import BaseDriver - - -class StorageDiskDriver(BaseDriver, StorageContract): - def put(self, location, contents): - with open(location, "w+") as file: - file.write(contents) - - def append(self, location, contents): - with open(location, "a+") as file: - file.write(contents) - - def get(self, location): - with open(location) as f: - return f.read() - - def delete(self, location): - try: - os.remove(location) - return True - except FileNotFoundError: - return False - - def exists(self, location): - return pathlib.Path(location).exists() - - def size(self, location): - try: - return os.path.getsize(location) - except FileNotFoundError: - return 0 - - def extension(self, location): - return pathlib.Path(location).suffix.replace(".", "") - - def url(self, location): - pass - - def name(self, location): - return pathlib.Path(location).name - - def upload(self, *args, **kwargs): - from wsgi import container - - return container.make(Upload).driver("disk").store(*args, **kwargs) - - def all(self): - pass - - def make_directory(self, location): - location = os.path.join(os.getcwd(), location) - if os.path.isdir(location): - return True - - os.mkdir(location) - return True - - def delete_directory(self, directory, force=False): - if force: - shutil.rmtree(directory) - return True - try: - pathlib.Path(directory).rmdir() - return True - except FileNotFoundError: - return True - - def move(self, old, new): - return shutil.move(old, new) diff --git a/src/masonite/drivers/upload/BaseUploadDriver.py b/src/masonite/drivers/upload/BaseUploadDriver.py deleted file mode 100644 index ba3860b29..000000000 --- a/src/masonite/drivers/upload/BaseUploadDriver.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Base upload driver module.""" - -import _io -from ...drivers import BaseDriver -from ...exceptions import FileTypeException -from ...helpers import random_string -from ...helpers import config - - -class BaseUploadDriver(BaseDriver): - """Base class that all upload drivers inherit from.""" - - accept_file_types = ("jpg", "jpeg", "png", "gif", "bmp") - - def accept(self, *args, **kwargs): - """Set file types to accept before uploading. - - Returns: - self - """ - - extensions = list(args) - - if "*" in extensions and len(extensions) > 1: - raise ValueError("When uses '*' isn't allowed accept other file type.") - - if extensions == ["*"]: - self.accept_file_types = None - else: - self.accept_file_types = args - - return self - - def validate_extension(self, filename): - """Check for valid file extenstions set with the 'accept' method. - - Arguments: - filename {string} -- The filename with file extension to validate. - - Raises: - FileTypeException -- Thrown if the specified file extension is incorrect. - """ - if self.accept_file_types is not None: - if not filename.lower().endswith(self.accept_file_types): - raise FileTypeException("The file extension is not supported.") - - return True - - def get_location(self, location=None): - """Get the location of where to upload. - - Keyword Arguments: - location {string} -- The path to upload to. If none then this will check for configuration settings. (default: {None}) - - Returns: - string -- Returns the location it uploaded to. - """ - if not location: - location = config("storage.drivers.disk.location") - - if "." in location: - location = location.split(".") - return config("storage.drivers")[location[0]]["location"][location[1]] - elif isinstance(location, str): - return location - elif isinstance(location, dict): - return list(location.values())[0] - - return location - - def get_name(self, fileitem): - return "{}.{}".format(random_string(25).lower(), self.get_extension(fileitem)) - - def get_extension(self, fileitem): - if isinstance(fileitem, _io.TextIOWrapper): - return fileitem.name.split(".")[-1] - else: - return fileitem.filename.split(".")[-1] diff --git a/src/masonite/drivers/upload/UploadDiskDriver.py b/src/masonite/drivers/upload/UploadDiskDriver.py deleted file mode 100644 index 881b62bce..000000000 --- a/src/masonite/drivers/upload/UploadDiskDriver.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Upload Disk Driver.""" - -import os -import _io - -from ...contracts import UploadContract -from ...drivers import BaseUploadDriver -from ...helpers.filesystem import make_directory - - -class UploadDiskDriver(BaseUploadDriver, UploadContract): - """Upload to and from the file system.""" - - file_location = None - - def __init__(self): - """Upload Disk Driver Constructor.""" - pass - - def store(self, fileitem, filename=None, location=None): - """Store the file onto a server. - - Arguments: - fileitem {cgi.Storage} -- Storage object. - - Keyword Arguments: - location {string} -- The location on disk you would like to store the file. (default: {None}) - filename {string} -- A new file name you would like to name the file. (default: {None}) - - Returns: - string -- Returns the file name just saved. - """ - - # use the new filename or get it from the fileitem - if filename is None: - filename = self.get_name(fileitem) - - # Check if is a valid extension - self.validate_extension(self.get_name(fileitem)) - - location = self.get_location(location) - - location = os.path.join(location, filename) - - make_directory(location) - - if isinstance(fileitem, _io.TextIOWrapper): - with open(location, "wb") as file: - file.write(bytes(fileitem.read(), "utf-8")) - else: - with open(location, "wb") as file: - file.write(fileitem.file.read()) - - self.file_location = location - - return filename diff --git a/src/masonite/drivers/upload/UploadS3Driver.py b/src/masonite/drivers/upload/UploadS3Driver.py deleted file mode 100644 index 0b4596d26..000000000 --- a/src/masonite/drivers/upload/UploadS3Driver.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Upload S3 Driver.""" - -import os - -from ...contracts import UploadContract -from ...drivers import BaseUploadDriver -from ...exceptions import DriverLibraryNotFound -from ...managers import UploadManager -from ...helpers import config - - -class UploadS3Driver(BaseUploadDriver, UploadContract): - """Amazon S3 Upload driver.""" - - def __init__(self, upload: UploadManager): - """Upload Disk Driver Constructor. - - Arguments: - UploadManager {masonite.managers.UploadManager} -- The Upload Manager object. - StorageConfig {config.storage} -- Storage configuration. - """ - self.upload = upload - self.config = config("storage") - - def store(self, fileitem, filename=None, location=None): - """Store the file into Amazon S3 server. - - Arguments: - fileitem {cgi.Storage} -- Storage object. - - Keyword Arguments: - location {string} -- The location on disk you would like to store the file. (default: {None}) - filename {string} -- A new file name you would like to name the file. (default: {None}) - - Raises: - DriverLibraryNotFound -- Raises when the boto3 library is not installed. - - Returns: - string -- Returns the file name just saved. - """ - try: - import boto3 - except ImportError: - raise DriverLibraryNotFound( - 'Could not find the "boto3" library. Please pip install this library by running "pip install boto3"' - ) - - driver = self.upload.driver("disk") - driver.accept_file_types = self.accept_file_types - driver.store(fileitem, filename=filename, location="storage/temp") - file_location = driver.file_location - - # use the new filename or get it from the fileitem - if filename is None: - filename = self.get_name(fileitem) - - # Check if is a valid extension - self.validate_extension(self.get_name(fileitem)) - - session = boto3.Session( - aws_access_key_id=self.config.DRIVERS["s3"]["client"], - aws_secret_access_key=self.config.DRIVERS["s3"]["secret"], - ) - - s3 = session.resource("s3") - - if location: - location = os.path.join(location, filename) - else: - location = os.path.join(filename) - - s3.meta.client.upload_file( - file_location, self.config.DRIVERS["s3"]["bucket"], location - ) - - return filename diff --git a/src/masonite/environment/__init__.py b/src/masonite/environment/__init__.py new file mode 100644 index 000000000..999f0dafb --- /dev/null +++ b/src/masonite/environment/__init__.py @@ -0,0 +1 @@ +from .environment import LoadEnvironment, env diff --git a/src/masonite/environment.py b/src/masonite/environment/environment.py similarity index 92% rename from src/masonite/environment.py rename to src/masonite/environment/environment.py index 4f49f037c..59c7de571 100644 --- a/src/masonite/environment.py +++ b/src/masonite/environment/environment.py @@ -49,6 +49,9 @@ def _load_environment(self, environment, override=False): def env(value, default="", cast=True): + """Helper to retrieve the value of an environment variable or returns + a default value. In addition, if type can be inferred then the value can be casted to the + inferred type.""" env_var = os.getenv(value, default) if not cast: diff --git a/src/masonite/essentials/helpers/__init__.py b/src/masonite/essentials/helpers/__init__.py new file mode 100644 index 000000000..94a710d1c --- /dev/null +++ b/src/masonite/essentials/helpers/__init__.py @@ -0,0 +1 @@ +from .hashid import hashid diff --git a/src/masonite/essentials/helpers/hashid.py b/src/masonite/essentials/helpers/hashid.py new file mode 100644 index 000000000..e9ec83a34 --- /dev/null +++ b/src/masonite/essentials/helpers/hashid.py @@ -0,0 +1,34 @@ +from hashids import Hashids + + +def hashid(*values, decode=False, min_length=7): + hash_class = Hashids(min_length=min_length) + if type(values[0]) == dict and decode: + new_dict = {} + for key, value in values[0].items(): + if hasattr(value, "value"): + value = value.value + + if value and hash_class.decode(value): + value = hash_class.decode(value) + + if type(value) == tuple: + value = value[0] + new_dict.update({key: value}) + return new_dict + + if not decode: + if isinstance(values[0], dict): + new_dic = {} + for key, value in values[0].items(): + if hasattr(value, "value"): + value = value.value + if str(value).isdigit(): + new_dic.update({key: hash_class.encode(int(value))}) + else: + new_dic.update({key: value}) + return new_dic + + return hash_class.encode(*values) + + return Hashids().decode(*values) diff --git a/src/masonite/essentials/middleware/HashIDMiddleware.py b/src/masonite/essentials/middleware/HashIDMiddleware.py new file mode 100644 index 000000000..18d810f6d --- /dev/null +++ b/src/masonite/essentials/middleware/HashIDMiddleware.py @@ -0,0 +1,17 @@ +from ...middleware import Middleware + +from ..helpers import hashid + + +class HashIDMiddleware(Middleware): + def before(self, request, response): + request.input_bag.query_string = hashid( + request.input_bag.query_string, decode=True + ) + request.params = hashid(request.params, decode=True) + request.input_bag.post_data = hashid(request.input_bag.post_data, decode=True) + return request + + def after(self, request, response): + + return request diff --git a/src/masonite/essentials/middleware/__init__.py b/src/masonite/essentials/middleware/__init__.py new file mode 100644 index 000000000..68f9c0f46 --- /dev/null +++ b/src/masonite/essentials/middleware/__init__.py @@ -0,0 +1 @@ +from .HashIDMiddleware import HashIDMiddleware diff --git a/src/masonite/essentials/providers/HashIDProvider.py b/src/masonite/essentials/providers/HashIDProvider.py new file mode 100644 index 000000000..9c8893dd2 --- /dev/null +++ b/src/masonite/essentials/providers/HashIDProvider.py @@ -0,0 +1,13 @@ +from ...providers import Provider +from ..helpers.hashid import hashid + + +class HashIDProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + self.application.make("view").share({"hashid": hashid}) + + def boot(self): + pass diff --git a/src/masonite/events/Event.py b/src/masonite/events/Event.py new file mode 100644 index 000000000..93d39b074 --- /dev/null +++ b/src/masonite/events/Event.py @@ -0,0 +1,81 @@ +""" Event Module """ + +import inspect + + +class Event: + def __init__(self, application): + """Event contructor + Arguments: + application - The Masonite application class + """ + self.application = application + self.events = {} + + def get_events(self): + return self.events + + def listen(self, event, listeners): + + if not isinstance(listeners, list): + listeners = [listeners] + + if event in self.events: + self.events[event] += listeners + else: + self.events.update({event: listeners}) + + return self + + def fire(self, event, *args, **kwargs): + if isinstance(event, str): + collected_events = self.collect_events(event) + for collected_event in collected_events: + for listener in self.events.get(collected_event, []): + listener().handle(event, *args, **kwargs) + return collected_events + else: + if inspect.isclass(event): + lookup = event + event = event() + else: + lookup = event.__class__ + for listener in self.events.get(lookup, []): + listener().handle(event, *args, **kwargs) + + return [event] + + def collect_events(self, fired_event): + collected_events = [] + for stored_event in self.events.keys(): + + if not isinstance(stored_event, str): + continue + + if stored_event == fired_event: + collected_events.append(fired_event) + + elif stored_event.endswith("*") and fired_event.startswith( + stored_event.replace("*", "") + ): + collected_events.append(stored_event) + + elif stored_event.startswith("*") and fired_event.endswith( + stored_event.replace("*", "") + ): + collected_events.append(stored_event) + + elif "*" in stored_event: + starts, end = stored_event.split("*") + if fired_event.startswith(starts) and fired_event.endswith(end): + collected_events.append(stored_event) + + return collected_events + + def subscribe(self, *listeners): + """Subscribe a specific listener object to the events system + Raises: + InvalidSubscriptionType -- raises when the subscribe attribute on the listener object is not a class. + """ + for listener in listeners: + listener.subscribe(self) diff --git a/src/masonite/events/Listener.py b/src/masonite/events/Listener.py new file mode 100644 index 000000000..b63e74ae4 --- /dev/null +++ b/src/masonite/events/Listener.py @@ -0,0 +1,2 @@ +class Listener: + pass diff --git a/src/masonite/events/__init__.py b/src/masonite/events/__init__.py new file mode 100644 index 000000000..d0703322f --- /dev/null +++ b/src/masonite/events/__init__.py @@ -0,0 +1 @@ +from .Event import Event diff --git a/src/masonite/events/commands/MakeEventCommand.py b/src/masonite/events/commands/MakeEventCommand.py new file mode 100644 index 000000000..62b533f53 --- /dev/null +++ b/src/masonite/events/commands/MakeEventCommand.py @@ -0,0 +1,39 @@ +"""New Event Command.""" +from cleo import Command +import inflection +import os + +from ...utils.filesystem import make_directory, get_module_dir, render_stub_file +from ...utils.location import base_path +from ...utils.str import as_filepath + + +class MakeEventCommand(Command): + """ + Creates a new event class. + + event + {name : Name of the event} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + + content = render_stub_file(self.get_stub_event_path(), name) + + relative_filename = os.path.join( + as_filepath(self.app.make("events.location")), name + ".py" + ) + filepath = base_path(relative_filename) + make_directory(filepath) + + with open(filepath, "w") as f: + f.write(content) + self.info(f"Event Created ({relative_filename})") + + def get_stub_event_path(self): + return os.path.join(get_module_dir(__file__), "../../stubs/events/Event.py") diff --git a/src/masonite/events/commands/MakeListenerCommand.py b/src/masonite/events/commands/MakeListenerCommand.py new file mode 100644 index 000000000..51cbae89e --- /dev/null +++ b/src/masonite/events/commands/MakeListenerCommand.py @@ -0,0 +1,38 @@ +"""New Listener Command.""" +from cleo import Command +import inflection +import os + +from ...utils.filesystem import make_directory, get_module_dir, render_stub_file +from ...utils.str import as_filepath +from ...utils.location import base_path + + +class MakeListenerCommand(Command): + """ + Creates a new listener class. + + listener + {name : Name of the listener} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + content = render_stub_file(self.get_path(), name) + + relative_filename = os.path.join( + as_filepath(self.app.make("listeners.location")), f"{name}.py" + ) + filepath = base_path(relative_filename) + make_directory(filepath) + + with open(filepath, "w") as f: + f.write(content) + self.info(f"Listener Created ({relative_filename})") + + def get_path(self): + return os.path.join(get_module_dir(__file__), "../../stubs/events/Listener.py") diff --git a/src/masonite/events/commands/__init__.py b/src/masonite/events/commands/__init__.py new file mode 100644 index 000000000..3a94a3945 --- /dev/null +++ b/src/masonite/events/commands/__init__.py @@ -0,0 +1,2 @@ +from .MakeEventCommand import MakeEventCommand +from .MakeListenerCommand import MakeListenerCommand diff --git a/src/masonite/events/providers/EventProvider.py b/src/masonite/events/providers/EventProvider.py new file mode 100644 index 000000000..429b3491b --- /dev/null +++ b/src/masonite/events/providers/EventProvider.py @@ -0,0 +1,18 @@ +from ...providers import Provider +from ..Event import Event +from ..commands import MakeListenerCommand, MakeEventCommand + + +class EventProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + event = Event(self.application) + self.application.make("commands").add( + MakeListenerCommand(self.application), MakeEventCommand(self.application) + ) + self.application.bind("event", event) + + def boot(self): + pass diff --git a/src/masonite/events/providers/__init__.py b/src/masonite/events/providers/__init__.py new file mode 100644 index 000000000..e2bbb71d2 --- /dev/null +++ b/src/masonite/events/providers/__init__.py @@ -0,0 +1 @@ +from .EventProvider import EventProvider diff --git a/src/masonite/exception_handler.py b/src/masonite/exception_handler.py deleted file mode 100644 index 8af355271..000000000 --- a/src/masonite/exception_handler.py +++ /dev/null @@ -1,176 +0,0 @@ -"""Exception Handler Module. - -A module for controlling exceptions handling when an error occurs doing executing -code in a Masonite application. These errors could are thrown during runtime. -""" - -import inspect -import os -import sys -import traceback - -from exceptionite.errors import Handler, SolutionsIntegration, StackOverflowIntegration - -from .app import App -from .exceptions import DumpException -from .helpers import config -from .listeners import BaseExceptionListener -from .request import Request -from .response import Response -from .view import View - -package_directory = os.path.dirname(os.path.realpath(__file__)) - - -class ExceptionHandler: - """Class for handling exceptions thrown during runtime.""" - - def __init__(self, app): - """ExceptionHandler constructor. Also responsible for loading static files into the container. - - Arguments: - app {masonite.app.App} -- Container object - """ - self._app = app - self.response = self._app.make(Response) - - self._register_static_files() - - def _register_static_files(self): - """Register static files into the container.""" - storage = config("storage") - if storage: - storage.STATICFILES.update( - {os.path.join(package_directory, "snippets/exceptions"): "_exceptions/"} - ) - - def load_exception(self, exception): - """Load the exception thrown into this handler. - - Arguments: - exception {Exception} -- This is the exception object thrown at runtime. - """ - self._exception = exception - - if self._app.has("Exception{}Handler".format(exception.__class__.__name__)): - - return self._app.make( - "Exception{}Handler".format(exception.__class__.__name__) - ).handle(exception) - - self.handle(exception) - - def run_listeners(self, exception, stacktraceback): - for exception_class in self._app.collect(BaseExceptionListener): - if ( - "*" in exception_class.listens - or exception.__class__ in exception_class.listens - ): - file, line = self.get_file_and_line(stacktraceback) - self._app.resolve(exception_class).handle(exception, file, line) - - def get_file_and_line(self, stacktraceback): - for stack in stacktraceback[::-1]: - if "site-packages" not in stack[0]: - return (stack[0], stack[1]) - - return (0, 0) - - def handle(self, exception): - """Render an exception view if the DEBUG configuration is True. Else this should not return anything. - - Returns: - None - """ - - stacktraceback = traceback.extract_tb(sys.exc_info()[2]) - self.run_listeners(exception, stacktraceback) - # Run Any Framework Exception Hooks - self._app.make("HookHandler").fire("*ExceptionHook") - request = self._app.make("Request") - response = self._app.make(Response) - - # Check if DEBUG is False - from config import application - - if not application.DEBUG: - response.status(500) - return - - handler = Handler(exception) - handler.integrate(SolutionsIntegration()) - handler.integrate( - StackOverflowIntegration(), - ) - - if "application/json" in request.header("Content-Type"): - stacktrace = [] - for trace in handler.stacktrace(): - stacktrace.append(trace.file + " line " + str(trace.lineno)) - - return response.json( - { - "Exeption": handler.exception(), - "Message": str(exception), - "traceback": stacktrace, - }, - status=500, - ) - - response.view(handler.render(), status=500) - - -class DD: - def __init__(self, container): - self.app = container - - def dump(self, *args): - dump_list = [] - for i, obj in enumerate(args): - dump_name = "ObjDump{}".format(i) - self.app.bind(dump_name, obj) - dump_list.append(dump_name) - self.app.bind("ObjDumpList", dump_list) - raise DumpException - - -class DumpHandler: - def __init__(self, view: View, request: Request, app: App, response: Response): - self.view = view - self.request = request - self.app = app - self.response = response - - def handle(self, _): - from masoniteorm.models import Model - - self.app.make("HookHandler").fire("*ExceptionHook") - - dump_objs = [] - for dump_name in self.app.make("ObjDumpList"): - obj = self.app.make(dump_name) - dump_objs.append( - { - "obj": obj, - "members": inspect.getmembers(obj, predicate=inspect.ismethod), - "properties": inspect.getmembers(obj), - } - ) - - self.response.view( - self.view.render( - "/masonite/snippets/exceptions/dump", - { - "objs": dump_objs, - "type": type, - "list": list, - "inspect": inspect, - "hasattr": hasattr, - "getattr": getattr, - "Model": Model, - "isinstance": isinstance, - "show_methods": (bool, str, list, dict), - "len": len, - }, - ) - ) diff --git a/src/masonite/exceptions/DD.py b/src/masonite/exceptions/DD.py new file mode 100644 index 000000000..ef40a8e80 --- /dev/null +++ b/src/masonite/exceptions/DD.py @@ -0,0 +1,15 @@ +from .exceptions import DumpException + + +class DD: + def __init__(self, container): + self.app = container + + def dump(self, *args): + dump_list = [] + for i, obj in enumerate(args): + dump_name = "ObjDump{}".format(i) + self.app.bind(dump_name, obj) + dump_list.append(dump_name) + self.app.bind("ObjDumpList", dump_list) + raise DumpException diff --git a/src/masonite/exceptions/DumpExceptionHandler.py b/src/masonite/exceptions/DumpExceptionHandler.py new file mode 100644 index 000000000..5308bc87c --- /dev/null +++ b/src/masonite/exceptions/DumpExceptionHandler.py @@ -0,0 +1,35 @@ +import inspect + + +class DumpExceptionHandler: + def __init__(self, application): + self.application = application + + def handle(self, exception): + dump_objs = [] + for dump_name in self.application.make("ObjDumpList"): + obj = self.application.make(dump_name) + dump_objs.append( + { + "obj": obj, + "members": inspect.getmembers(obj, predicate=inspect.ismethod), + "properties": inspect.getmembers(obj), + } + ) + + return self.application.make("response").view( + self.application.make("view").render( + "/masonite/templates/dump", + { + "objs": dump_objs, + "type": type, + "list": list, + "inspect": inspect, + "hasattr": hasattr, + "getattr": getattr, + "isinstance": isinstance, + "show_methods": (bool, str, list, dict), + "len": len, + }, + ) + ) diff --git a/src/masonite/exceptions/ExceptionHandler.py b/src/masonite/exceptions/ExceptionHandler.py new file mode 100644 index 000000000..7f39edef6 --- /dev/null +++ b/src/masonite/exceptions/ExceptionHandler.py @@ -0,0 +1,65 @@ +from exceptionite.errors import Handler, StackOverflowIntegration, SolutionsIntegration + + +class ExceptionHandler: + def __init__(self, application, driver_config=None): + self.application = application + self.drivers = {} + self.driver_config = driver_config or {} + self.options = {} + + def set_options(self, options): + self.options = options + return self + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + + def set_configuration(self, config): + self.driver_config = config + return self + + def get_driver(self, name=None): + if name is None: + return self.drivers[self.driver_config.get("default")] + return self.drivers[name] + + def get_config_options(self, driver=None): + if driver is None: + return self.driver_config[self.driver_config.get("default")] + + return self.driver_config.get(driver, {}) + + def handle(self, exception): + response = self.application.make("response") + request = self.application.make("request") + self.application.make("event").fire( + f"masonite.exception.{exception.__class__.__name__}", exception + ) + + if self.application.has(f"{exception.__class__.__name__}Handler"): + return self.application.make( + f"{exception.__class__.__name__}Handler" + ).handle(exception) + + if hasattr(exception, "get_response"): + return response.view(exception.get_response(), exception.get_status()) + + handler = Handler(exception) + if self.options.get("handlers.stack_overflow"): + handler.integrate(StackOverflowIntegration()) + if self.options.get("handlers.solutions"): + handler.integrate(SolutionsIntegration()) + handler.context( + { + "WSGI": { + "Path": request.get_path(), + "Input": request.input_bag.all_as_values() or None, + # 'Parameters': request.url_params, + "Request Method": request.get_request_method(), + }, + "Headers": request.header_bag.to_dict(), + } + ) + + return response.view(handler.render(), status=500) diff --git a/src/masonite/exceptions/__init__.py b/src/masonite/exceptions/__init__.py new file mode 100644 index 000000000..2a9ae9f5e --- /dev/null +++ b/src/masonite/exceptions/__init__.py @@ -0,0 +1,32 @@ +from .ExceptionHandler import ExceptionHandler +from .DumpExceptionHandler import DumpExceptionHandler +from .DD import DD +from .exceptions import ( + InvalidRouteCompileException, + RouteMiddlewareNotFound, + ContainerError, + MissingContainerBindingNotFound, + StrictContainerException, + ResponseError, + InvalidHTTPStatusCode, + RequiredContainerBindingNotFound, + ViewException, + RouteNotFoundException, + DumpException, + InvalidSecretKey, + InvalidCSRFToken, + NotificationException, + InvalidToken, + ProjectLimitReached, + ProjectProviderTimeout, + ProjectProviderHttpError, + ProjectTargetNotEmpty, + MixFileNotFound, + MixManifestNotFound, + InvalidConfigurationLocation, + InvalidConfigurationSetup, + InvalidPackageName, + LoaderNotFound, + QueueException, + AmbiguousError, +) diff --git a/src/masonite/exceptions.py b/src/masonite/exceptions/exceptions.py similarity index 62% rename from src/masonite/exceptions.py rename to src/masonite/exceptions/exceptions.py index b70d8dd93..73f42e0a9 100644 --- a/src/masonite/exceptions.py +++ b/src/masonite/exceptions/exceptions.py @@ -54,6 +54,10 @@ class InvalidSecretKey(Exception): pass +class InvalidToken(Exception): + pass + + class StrictContainerException(Exception): pass @@ -104,3 +108,52 @@ class ProjectProviderHttpError(Exception): class ProjectTargetNotEmpty(Exception): pass + + +class NotificationException(Exception): + pass + + +class AuthorizationException(Exception): + def __init__(self, message, status): + super().__init__(self) + self.message = message or "Action not authorized" + self.status = status or 403 + + def get_response(self): + return self.message + + def get_status(self): + return self.status + + +class GateDoesNotExist(Exception): + pass + + +class PolicyDoesNotExist(Exception): + pass + + +class MixManifestNotFound(Exception): + pass + + +class MixFileNotFound(Exception): + pass + + +class InvalidConfigurationLocation(Exception): + pass + + +class InvalidConfigurationSetup(Exception): + pass + + +class InvalidPackageName(Exception): + pass + + +class LoaderNotFound(Exception): + pass diff --git a/src/masonite/facades/Auth.py b/src/masonite/facades/Auth.py new file mode 100644 index 000000000..ddeb848d7 --- /dev/null +++ b/src/masonite/facades/Auth.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Auth(metaclass=Facade): + key = "auth" diff --git a/src/masonite/facades/Config.py b/src/masonite/facades/Config.py new file mode 100644 index 000000000..5b17e2ecd --- /dev/null +++ b/src/masonite/facades/Config.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Config(metaclass=Facade): + key = "config" diff --git a/src/masonite/facades/Facade.py b/src/masonite/facades/Facade.py new file mode 100644 index 000000000..a214d9db0 --- /dev/null +++ b/src/masonite/facades/Facade.py @@ -0,0 +1,5 @@ +class Facade(type): + def __getattr__(self, attribute, *args, **kwargs): + from wsgi import application + + return getattr(application.make(self.key), attribute) diff --git a/src/masonite/facades/Gate.py b/src/masonite/facades/Gate.py new file mode 100644 index 000000000..c3b9fd31c --- /dev/null +++ b/src/masonite/facades/Gate.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Gate(metaclass=Facade): + key = "gate" diff --git a/src/masonite/facades/Hash.py b/src/masonite/facades/Hash.py new file mode 100644 index 000000000..bda5fa0fd --- /dev/null +++ b/src/masonite/facades/Hash.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Hash(metaclass=Facade): + key = "hash" diff --git a/src/masonite/facades/Loader.py b/src/masonite/facades/Loader.py new file mode 100644 index 000000000..2232de98a --- /dev/null +++ b/src/masonite/facades/Loader.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Loader(metaclass=Facade): + key = "loader" diff --git a/src/masonite/facades/Mail.py b/src/masonite/facades/Mail.py new file mode 100644 index 000000000..dd44b13e2 --- /dev/null +++ b/src/masonite/facades/Mail.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Mail(metaclass=Facade): + key = "mail" diff --git a/src/masonite/facades/Notification.py b/src/masonite/facades/Notification.py new file mode 100644 index 000000000..f045ff371 --- /dev/null +++ b/src/masonite/facades/Notification.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Notification(metaclass=Facade): + key = "notification" diff --git a/src/masonite/facades/Request.py b/src/masonite/facades/Request.py new file mode 100644 index 000000000..cea382bbe --- /dev/null +++ b/src/masonite/facades/Request.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Request(metaclass=Facade): + key = "request" diff --git a/src/masonite/facades/Response.py b/src/masonite/facades/Response.py new file mode 100644 index 000000000..1981474bd --- /dev/null +++ b/src/masonite/facades/Response.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Response(metaclass=Facade): + key = "response" diff --git a/src/masonite/facades/Session.py b/src/masonite/facades/Session.py new file mode 100644 index 000000000..b97bafc18 --- /dev/null +++ b/src/masonite/facades/Session.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Session(metaclass=Facade): + key = "session" diff --git a/src/masonite/facades/Url.py b/src/masonite/facades/Url.py new file mode 100644 index 000000000..77ec325a9 --- /dev/null +++ b/src/masonite/facades/Url.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class Url(metaclass=Facade): + key = "url" diff --git a/src/masonite/facades/View.py b/src/masonite/facades/View.py new file mode 100644 index 000000000..dcfe7aa74 --- /dev/null +++ b/src/masonite/facades/View.py @@ -0,0 +1,5 @@ +from .Facade import Facade + + +class View(metaclass=Facade): + key = "view" diff --git a/src/masonite/facades/__init__.py b/src/masonite/facades/__init__.py new file mode 100644 index 000000000..fa76a21a2 --- /dev/null +++ b/src/masonite/facades/__init__.py @@ -0,0 +1,13 @@ +from .Facade import Facade +from .Request import Request +from .Response import Response +from .Mail import Mail +from .Hash import Hash +from .Url import Url +from .Session import Session +from .View import View +from .Gate import Gate +from .Auth import Auth +from .Config import Config +from .Loader import Loader +from .Notification import Notification diff --git a/src/masonite/filesystem/File.py b/src/masonite/filesystem/File.py new file mode 100644 index 000000000..73de6f938 --- /dev/null +++ b/src/masonite/filesystem/File.py @@ -0,0 +1,26 @@ +import hashlib +import os + + +class File: + def __init__(self, content, filename=None): + self.content = content + self.filename = filename + + def path(self): + pass + + def extension(self): + return os.path.splitext(self.filename)[1] + + def name(self): + return self.filename + + def stream(self): + return self.content + + def hash_path_name(self): + return f"{self.hash_name()}{self.extension()}" + + def hash_name(self): + return hashlib.sha1(bytes(self.name(), "utf-8")).hexdigest() diff --git a/src/masonite/filesystem/FileStream.py b/src/masonite/filesystem/FileStream.py new file mode 100644 index 000000000..981bbe368 --- /dev/null +++ b/src/masonite/filesystem/FileStream.py @@ -0,0 +1,16 @@ +import os + + +class FileStream: + def __init__(self, stream, name=None): + self.stream = stream + self._name = name + + def path(self): + return self.stream.name + + def extension(self): + return os.path.splitext(self._name or self.path())[1] + + def name(self): + return self._name or os.path.basename(self.path()) diff --git a/src/masonite/filesystem/Storage.py b/src/masonite/filesystem/Storage.py new file mode 100644 index 000000000..948dc7a14 --- /dev/null +++ b/src/masonite/filesystem/Storage.py @@ -0,0 +1,35 @@ +class Storage: + def __init__(self, application, store_config=None): + self.application = application + self.drivers = {} + self.store_config = store_config or {} + self.options = {} + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + + def set_configuration(self, config): + self.store_config = config + return self + + def get_driver(self, name=None): + if name is None: + return self.drivers[self.store_config.get("default")] + return self.drivers[name] + + def get_store_config(self, name=None): + if name is None or name == "default": + return self.store_config.get(self.store_config.get("default")) + + return self.store_config.get(name) + + def get_config_options(self, name=None): + if name is None or name == "default": + return self.store_config.get(self.store_config.get("default")) + + return self.store_config.get(name) + + def disk(self, name="default"): + store_config = self.get_config_options(name) + driver = self.get_driver(self.get_config_options(name).get("driver")) + return driver.set_options(store_config) diff --git a/src/masonite/filesystem/UploadedFile.py b/src/masonite/filesystem/UploadedFile.py new file mode 100644 index 000000000..44538ea01 --- /dev/null +++ b/src/masonite/filesystem/UploadedFile.py @@ -0,0 +1,30 @@ +import os +import hashlib + + +class UploadedFile: + def __init__(self, filename, content): + self.filename = filename + self.content = content + + def extension(self): + return os.path.splitext(self.filename)[1] + + @property + def name(self): + return self.filename + + def path_name(self): + return f"{self.name()}{self.extension()}" + + def hash_path_name(self): + return f"{self.hash_name()}{self.extension()}" + + def stream(self): + return self.content + + def hash_name(self): + return hashlib.sha1(bytes(self.name(), "utf-8")).hexdigest() + + def get_content(self): + return self.content diff --git a/src/masonite/filesystem/__init__.py b/src/masonite/filesystem/__init__.py new file mode 100644 index 000000000..cc1613b6a --- /dev/null +++ b/src/masonite/filesystem/__init__.py @@ -0,0 +1,3 @@ +from .Storage import Storage +from .File import File +from .UploadedFile import UploadedFile diff --git a/src/masonite/filesystem/drivers/AmazonS3Driver.py b/src/masonite/filesystem/drivers/AmazonS3Driver.py new file mode 100644 index 000000000..566e31d43 --- /dev/null +++ b/src/masonite/filesystem/drivers/AmazonS3Driver.py @@ -0,0 +1,146 @@ +import os +from shutil import copyfile, move +from ..FileStream import FileStream +import uuid + + +class AmazonS3Driver: + def __init__(self, application): + self.application = application + self.options = {} + self.connection = None + + def set_options(self, options): + self.options = options + return self + + def get_connection(self): + try: + import boto3 + except ImportError: + raise ModuleNotFoundError( + "Could not find the 'boto3' library. Run 'pip install boto3' to fix this." + ) + + if not self.connection: + self.connection = boto3.Session( + aws_access_key_id=self.options.get("client"), + aws_secret_access_key=self.options.get("secret"), + region_name=self.options.get("region"), + ) + + return self.connection + + def get_bucket(self): + return self.options.get("bucket") + + def get_name(self, path, alias): + extension = os.path.splitext(path)[1] + return f"{alias}{extension}" + + def put(self, file_path, content): + self.get_connection().resource("s3").Bucket(self.get_bucket()).put_object( + Key=file_path, Body=content + ) + return content + + def put_file(self, file_path, content, name=None): + file_name = self.get_name(content.name, name or str(uuid.uuid4())) + + if hasattr(content, "get_content"): + content = content.get_content() + + self.get_connection().resource("s3").Bucket(self.get_bucket()).put_object( + Key=os.path.join(file_path, file_name), Body=content + ) + return os.path.join(file_path, file_name) + + def get(self, file_path): + try: + return ( + self.get_connection() + .resource("s3") + .Bucket(self.get_bucket()) + .Object(file_path) + .get() + .get("Body") + .read() + .decode("utf-8") + ) + except self.missing_file_exceptions(): + pass + + def missing_file_exceptions(self): + import boto3 + + return (boto3.exceptions.botocore.errorfactory.ClientError,) + + def exists(self, file_path): + try: + self.get_connection().resource("s3").Bucket(self.get_bucket()).Object( + file_path + ).get().get("Body").read() + return True + except self.missing_file_exceptions(): + return False + + def missing(self, file_path): + return not self.exists(file_path) + + def stream(self, file_path): + return FileStream( + self.get_connection() + .resource("s3") + .Bucket(self.get_bucket()) + .Object(file_path) + .get() + .get("Body") + .read(), + file_path, + ) + + def copy(self, from_file_path, to_file_path): + copy_source = {"Bucket": self.get_bucket(), "Key": from_file_path} + self.get_connection().resource("s3").meta.client.copy( + copy_source, self.get_bucket(), to_file_path + ) + + def move(self, from_file_path, to_file_path): + self.copy(from_file_path, to_file_path) + self.delete(from_file_path) + + def prepend(self, file_path, content): + value = self.get(file_path) + content = content + value + self.put(file_path, content) + return content + + def append(self, file_path, content): + value = self.get(file_path) or "" + value += content + self.put(file_path, content) + + def delete(self, file_path): + return ( + self.get_connection() + .resource("s3") + .Object(self.get_bucket(), file_path) + .delete() + ) + + def store(self, file, name=None): + full_path = name or file.hash_path_name() + self.get_connection().resource("s3").Bucket(self.get_bucket()).put_object( + Key=full_path, Body=file.stream() + ) + return full_path + + def make_file_path_if_not_exists(self, file_path): + if not os.path.isfile(file_path): + if not os.path.exists(os.path.dirname(file_path)): + # Create the path to the model if it does not exist + os.makedirs(os.path.dirname(file_path)) + + return True + + return False diff --git a/src/masonite/filesystem/drivers/LocalDriver.py b/src/masonite/filesystem/drivers/LocalDriver.py new file mode 100644 index 000000000..5a48a10d0 --- /dev/null +++ b/src/masonite/filesystem/drivers/LocalDriver.py @@ -0,0 +1,105 @@ +import os +from shutil import copyfile, move +from ..FileStream import FileStream +import uuid +import os + + +class LocalDriver: + def __init__(self, application): + self.application = application + self.options = {} + + def set_options(self, options): + self.options = options + return self + + def get_path(self, path): + file_path = os.path.join(self.options.get("path"), path) + self.make_file_path_if_not_exists(file_path) + return file_path + + def get_name(self, path, alias): + extension = os.path.splitext(path)[1] + return f"{alias}{extension}" + + def put(self, file_path, content): + with open(self.get_path(os.path.join(file_path)), "w") as f: + f.write(content) + return content + + def put_file(self, file_path, content, name=None): + file_name = self.get_name(content.name, name or str(uuid.uuid4())) + + if hasattr(content, "get_content"): + content = content.get_content() + + if isinstance(content, str): + content = bytes(content, "utf-8") + + with open(self.get_path(os.path.join(file_path, file_name)), "wb") as f: + f.write(content) + + return os.path.join(file_path, file_name) + + def get(self, file_path): + try: + with open(self.get_path(file_path), "r") as f: + content = f.read() + + return content + except FileNotFoundError: + return None + + def exists(self, file_path): + return os.path.exists(self.get_path(file_path)) + + def missing(self, file_path): + return not self.exists(file_path) + + def stream(self, file_path): + with open(self.get_path(file_path), "r") as f: + content = f + return FileStream(content) + + def copy(self, from_file_path, to_file_path): + return copyfile(from_file_path, to_file_path) + + def move(self, from_file_path, to_file_path): + return move(self.get_path(from_file_path), self.get_path(to_file_path)) + + def prepend(self, file_path, content): + value = self.get(file_path) + content = content + value + self.put(file_path, content) + return content + + def append(self, file_path, content): + with open(self.get_path(file_path), "a") as f: + f.write(content) + return content + + def delete(self, file_path): + return os.remove(self.get_path(file_path)) + + def make_directory(self, directory): + pass + + def store(self, file, name=None): + if name: + name = f"{name}{file.extension()}" + full_path = self.get_path(name or file.hash_path_name()) + with open(full_path, "wb") as f: + f.write(file.stream()) + + return full_path + + def make_file_path_if_not_exists(self, file_path): + if not os.path.isfile(file_path): + if not os.path.exists(os.path.dirname(file_path)): + # Create the path to the model if it does not exist + os.makedirs(os.path.dirname(file_path)) + + return True + + return False diff --git a/src/masonite/filesystem/drivers/__init__.py b/src/masonite/filesystem/drivers/__init__.py new file mode 100644 index 000000000..668aac8db --- /dev/null +++ b/src/masonite/filesystem/drivers/__init__.py @@ -0,0 +1,2 @@ +from .LocalDriver import LocalDriver +from .AmazonS3Driver import AmazonS3Driver diff --git a/src/masonite/filesystem/providers/StorageProvider.py b/src/masonite/filesystem/providers/StorageProvider.py new file mode 100644 index 000000000..cb92cdb7f --- /dev/null +++ b/src/masonite/filesystem/providers/StorageProvider.py @@ -0,0 +1,20 @@ +from ...providers import Provider +from ..Storage import Storage +from ...configuration import config +from ..drivers import LocalDriver, AmazonS3Driver + + +class StorageProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + storage = Storage(self.application).set_configuration( + config("filesystem.disks") + ) + storage.add_driver("file", LocalDriver(self.application)) + storage.add_driver("s3", AmazonS3Driver(self.application)) + self.application.bind("storage", storage) + + def boot(self): + pass diff --git a/src/masonite/filesystem/providers/__init__.py b/src/masonite/filesystem/providers/__init__.py new file mode 100644 index 000000000..c788cf890 --- /dev/null +++ b/src/masonite/filesystem/providers/__init__.py @@ -0,0 +1 @@ +from .StorageProvider import StorageProvider diff --git a/src/masonite/foundation/Application.py b/src/masonite/foundation/Application.py new file mode 100644 index 000000000..cb0107f10 --- /dev/null +++ b/src/masonite/foundation/Application.py @@ -0,0 +1,77 @@ +import os +import sys +from ..container import Container + + +class Application(Container): + def __init__(self, base_path=None): + self.base_path = base_path + self.storage_path = None + self.response_handler = None + self.providers = [] + + def set_response_handler(self, response_handler): + self.response_handler = response_handler + + def get_response_handler(self): + return self.response_handler + + def register_providers(self, *providers): + for provider in providers: + provider = provider(self) + provider.register() + return self + + def use_storage_path(self, path): + self.storage_path = path + + def get_storage_path(self): + return self.storage_path + + def add_providers(self, *providers): + for provider in providers: + provider = provider(self) + provider.register() + self.providers.append(provider) + + return self + + def set_controller_locations(self, location): + self._controller_locations = location + + def get_controller_locations(self, location): + return self._controller_locations + + def get_providers(self): + return self.providers + + def __call__(self, *args, **kwargs): + return self.response_handler(*args, **kwargs) + + def is_dev(self): + """Check if app is running in development mode.""" + return os.getenv("APP_ENV") == "development" + + def is_production(self): + """Check if app is running in production mode.""" + return os.getenv("APP_ENV") == "production" + + def is_running_tests(self): + """Check if app is running tests.""" + + return "pytest" in sys.modules + + def is_running_in_console(self): + """Check if application is running in console. This is useful to only run some providers + logic when used in console. We can differenciate if the application is being served or + if an application command is ran in console.""" + if len(sys.argv) > 1: + return sys.argv[1] != "serve" + return True + + def environment(self): + """Helper to get current environment.""" + if self.is_running_tests(): + return "testing" + else: + return os.getenv("APP_ENV") diff --git a/src/masonite/foundation/Kernel.py b/src/masonite/foundation/Kernel.py new file mode 100644 index 000000000..72180b560 --- /dev/null +++ b/src/masonite/foundation/Kernel.py @@ -0,0 +1,79 @@ +import os +from cleo import Application as CommandApplication + +from .response_handler import response_handler +from ..commands import ( + TinkerCommand, + CommandCapsule, + KeyCommand, + ServeCommand, + QueueWorkCommand, + QueueRetryCommand, + QueueTableCommand, + QueueFailedCommand, + AuthCommand, + MakePolicyCommand, + MakeControllerCommand, + MakeJobCommand, + MakeMailableCommand, + MakeProviderCommand, + PublishPackageCommand, +) +from ..environment import LoadEnvironment +from ..middleware import MiddlewareCapsule +from ..routes import Router +from ..loader import Loader + +from ..tests.HttpTestResponse import HttpTestResponse +from ..tests.TestResponseCapsule import TestResponseCapsule + + +class Kernel: + def __init__(self, app): + self.application = app + + def register(self): + self.load_environment() + self.register_framework() + self.register_commands() + self.register_testing() + + def load_environment(self): + LoadEnvironment() + + def register_framework(self): + self.application.set_response_handler(response_handler) + self.application.use_storage_path( + os.path.join(self.application.base_path, "storage") + ) + self.application.bind("middleware", MiddlewareCapsule()) + self.application.bind( + "router", + Router(), + ) + self.application.bind("loader", Loader()) + + def register_commands(self): + self.application.bind( + "commands", + CommandCapsule(CommandApplication("Masonite Version:", "4.0")).add( + TinkerCommand(), + KeyCommand(), + ServeCommand(self.application), + QueueWorkCommand(self.application), + QueueRetryCommand(self.application), + QueueFailedCommand(), + QueueTableCommand(), + AuthCommand(self.application), + MakePolicyCommand(self.application), + MakeControllerCommand(self.application), + MakeJobCommand(self.application), + MakeMailableCommand(self.application), + MakeProviderCommand(self.application), + PublishPackageCommand(self.application), + ), + ) + + def register_testing(self): + test_response = TestResponseCapsule(HttpTestResponse) + self.application.bind("tests.response", test_response) diff --git a/src/masonite/foundation/__init__.py b/src/masonite/foundation/__init__.py new file mode 100644 index 000000000..56b9a796f --- /dev/null +++ b/src/masonite/foundation/__init__.py @@ -0,0 +1,3 @@ +from .Application import Application +from .Kernel import Kernel +from .response_handler import response_handler diff --git a/src/masonite/wsgi.py b/src/masonite/foundation/response_handler.py similarity index 56% rename from src/masonite/wsgi.py rename to src/masonite/foundation/response_handler.py index 84522fd66..433fdb86b 100644 --- a/src/masonite/wsgi.py +++ b/src/masonite/foundation/response_handler.py @@ -8,7 +8,9 @@ def response_handler(environ, start_response): Returns: WSGI Response """ - from wsgi import container + from wsgi import application + + application.bind("environ", environ) """Add Environ To Service Container Add the environ to the service container. The environ is generated by the @@ -16,17 +18,15 @@ def response_handler(environ, start_response): incoming requests """ - container.bind("Environ", environ) - - """Execute All Service Providers That Require The WSGI Server - Run all service provider boot methods if the wsgi attribute is true. - """ + # """Execute All Service Providers That Require The WSGI Server + # Run all service provider boot methods if the wsgi attribute is true. + # """ try: - for provider in container.make("WSGIProviders"): - container.resolve(provider.boot) + for provider in application.get_providers(): + application.resolve(provider.boot) except Exception as e: - container.make("ExceptionHandler").load_exception(e) + application.make("exception_handler").handle(e) """We Are Ready For Launch If we have a solid response and not redirecting then we need to return @@ -35,11 +35,12 @@ def response_handler(environ, start_response): to next. """ - from masonite.response import Response + _, response = application.make("request"), application.make("response") - response = container.make(Response) - - start_response(response.get_status_code(), response.get_and_reset_headers()) + start_response( + response.get_status_code(), + response.get_headers() + response.cookie_jar.render_response(), + ) """Final Step This will take the data variable from the Service Container and return @@ -48,7 +49,7 @@ def response_handler(environ, start_response): return iter([response.get_response_content()]) -def package_response_handler(environ, start_response): +def testcase_handler(application, environ, start_response, exception_handling=True): """The WSGI Application Server. Arguments: @@ -58,7 +59,9 @@ def package_response_handler(environ, start_response): Returns: WSGI Response """ - from wsgi import container + from wsgi import application + + application.bind("environ", environ) """Add Environ To Service Container Add the environ to the service container. The environ is generated by the @@ -66,17 +69,17 @@ def package_response_handler(environ, start_response): incoming requests """ - container.bind("Environ", environ) - - """Execute All Service Providers That Require The WSGI Server - Run all service provider boot methods if the wsgi attribute is true. - """ + # """Execute All Service Providers That Require The WSGI Server + # Run all service provider boot methods if the wsgi attribute is true. + # """ try: - for provider in container.make("WSGIProviders"): - container.resolve(provider.boot) + for provider in application.get_providers(): + application.resolve(provider.boot) except Exception as e: - container.make("ExceptionHandler").load_exception(e) + if not exception_handling: + raise e + application.make("exception_handler").handle(e) """We Are Ready For Launch If we have a solid response and not redirecting then we need to return @@ -85,14 +88,15 @@ def package_response_handler(environ, start_response): to next. """ - from src.masonite.response import Response + request, response = application.make("request"), application.make("response") - response = container.make(Response) - - start_response(response.get_status_code(), response.get_and_reset_headers()) + start_response( + response.get_status_code(), + response.get_headers() + response.cookie_jar.render_response(), + ) """Final Step This will take the data variable from the Service Container and return it to the WSGI server. """ - return iter([response.get_response_content()]) + return (request, response) diff --git a/src/masonite/hashing/Hash.py b/src/masonite/hashing/Hash.py new file mode 100644 index 000000000..68fe04fe6 --- /dev/null +++ b/src/masonite/hashing/Hash.py @@ -0,0 +1,48 @@ +class Hash: + def __init__(self, application, driver_config=None): + self.application = application + self.drivers = {} + self.driver_config = driver_config or {} + self.options = {} + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + + def set_configuration(self, config): + self.driver_config = config + return self + + def get_driver(self, name=None): + if name is None: + return self.drivers[self.driver_config.get("default")] + return self.drivers[name] + + def get_config_options(self, driver=None): + if driver is None: + return self.driver_config.get(self.driver_config.get("default"), {}) + return self.driver_config.get(driver, {}) + + def make(self, string, options={}, driver=None): + """Hash a string based on configured hashing protocol.""" + return ( + self.get_driver(driver) + .set_options(options or self.get_config_options(driver)) + .make(string) + ) + + def check(self, plain_string, hashed_string, options={}, driver=None): + """Verify that a given string matches its hashed version (based on configured hashing protocol).""" + return ( + self.get_driver(driver) + .set_options(options or self.get_config_options(driver)) + .check(plain_string, hashed_string) + ) + + def needs_rehash(self, hashed_string, options={}, driver=None): + """Verify that a given hash needs to be hashed again because parameters for generating + the hash have changed.""" + return ( + self.get_driver(driver) + .set_options(options or self.get_config_options(driver)) + .needs_rehash(hashed_string) + ) diff --git a/src/masonite/hashing/__init__.py b/src/masonite/hashing/__init__.py new file mode 100644 index 000000000..a9b94a26c --- /dev/null +++ b/src/masonite/hashing/__init__.py @@ -0,0 +1 @@ +from .Hash import Hash diff --git a/src/masonite/hashing/drivers/Argon2Hasher.py b/src/masonite/hashing/drivers/Argon2Hasher.py new file mode 100644 index 000000000..0e8c33d35 --- /dev/null +++ b/src/masonite/hashing/drivers/Argon2Hasher.py @@ -0,0 +1,34 @@ +class Argon2Hasher: + def __init__(self, options={}): + self.options = options + + def set_options(self, options): + self.options = options + return self + + def _get_password_hasher(self): + try: + import argon2 + except ImportError: + raise ModuleNotFoundError( + "Could not find the 'argon2' library. Run 'pip install argon2-cffi' to fix this." + ) + + memory = self.options.get("memory", argon2.DEFAULT_MEMORY_COST) + threads = self.options.get("threads", argon2.DEFAULT_PARALLELISM) + time = self.options.get("time", argon2.DEFAULT_TIME_COST) + return argon2.PasswordHasher( + memory_cost=memory, parallelism=threads, time_cost=time + ) + + def make(self, string): + ph = self._get_password_hasher() + return str(ph.hash(bytes(string, "utf-8"))) + + def check(self, plain_string, hashed_string): + ph = self._get_password_hasher() + return ph.verify(hashed_string, bytes(plain_string, "utf-8")) + + def needs_rehash(self, hashed_string): + ph = self._get_password_hasher() + return ph.check_needs_rehash(hashed_string) diff --git a/src/masonite/hashing/drivers/BcryptHasher.py b/src/masonite/hashing/drivers/BcryptHasher.py new file mode 100644 index 000000000..dc5d228fc --- /dev/null +++ b/src/masonite/hashing/drivers/BcryptHasher.py @@ -0,0 +1,28 @@ +import bcrypt + + +class BcryptHasher: + def __init__(self, options={}): + self.options = options + + def set_options(self, options): + self.options = options + return self + + def make(self, string): + rounds = self.options.get("rounds", 12) + salt = bcrypt.gensalt(rounds=rounds) + return bcrypt.hashpw(bytes(string, "utf-8"), salt) + + def check(self, plain_string, hashed_string): + if not isinstance(hashed_string, bytes): + hashed_string = bytes(hashed_string or "", "utf-8") + return bcrypt.checkpw(bytes(plain_string, "utf-8"), hashed_string) + + def needs_rehash(self, hashed_string): + # Bcrypt hashes have the format $2b${rounds}${salt}{checksum}. rounds is encoded as + # 2 zero-padded decimal digits. The prefix (2b) is never modified in make() function so we + # can assume that rounds value used when generating the hash is located at [4:6] indexes + # of the hash. + old_rounds_value = int(hashed_string[4:6]) + return old_rounds_value != self.options.get("rounds", 12) diff --git a/src/masonite/hashing/drivers/__init__.py b/src/masonite/hashing/drivers/__init__.py new file mode 100644 index 000000000..694bd9102 --- /dev/null +++ b/src/masonite/hashing/drivers/__init__.py @@ -0,0 +1,2 @@ +from .BcryptHasher import BcryptHasher +from .Argon2Hasher import Argon2Hasher diff --git a/src/masonite/headers/HeaderBag.py b/src/masonite/headers/HeaderBag.py index a33e4c8b8..c21133f97 100644 --- a/src/masonite/headers/HeaderBag.py +++ b/src/masonite/headers/HeaderBag.py @@ -36,5 +36,12 @@ def load(self, environ): if key.startswith("HTTP_"): self.add(Header(key, value)) + def to_dict(self): + dic = {} + for name, header in self.bag.items(): + dic.update({name: header.value}) + + return dic + def __getitem__(self, key): return self.bag[self.convert_name(key)] diff --git a/src/masonite/helpers/Extendable.py b/src/masonite/helpers/Extendable.py deleted file mode 100644 index 43da3835b..000000000 --- a/src/masonite/helpers/Extendable.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Extendable Module.""" - -import inspect - - -class Extendable: - """Add the ability to extend classes on the fly.""" - - def extend(self, key, obj=None): - """Extend the current class with an object. - - This essentially extends a class on the fly. - - Arguments: - key {string} -- The name of the method you want to set - - Keyword Arguments: - obj {object} -- Any function, method or class (default: {None}) - - Returns: - self - """ - # If both key and an object is passed - if obj: - if inspect.ismethod(obj): - obj = obj.__func__ - - setattr(self, key, obj.__get__(self)) - return self - - # Extend all of a classes methods into this class - if inspect.isclass(key): - for method in inspect.getmembers(key, inspect.isfunction): - setattr(self, method[0], method[1].__get__(self)) - - # Extend a function into this class - elif inspect.isfunction(key): - setattr(self, key.__name__, key.__get__(self)) - elif inspect.ismethod(obj): - setattr(self, key.__name__, key.__func__.__get__(self)) - return self diff --git a/src/masonite/helpers/__init__.py b/src/masonite/helpers/__init__.py index af46a560d..96b8bde85 100644 --- a/src/masonite/helpers/__init__.py +++ b/src/masonite/helpers/__init__.py @@ -1,16 +1,5 @@ -from .static import static -from .password import password -from .misc import ( - random_string, - dot, - clean_request_input, - HasColoredCommands, - Compact as compact, - deprecated, -) -from .Extendable import Extendable -from .time import cookie_expire_time, parse_human_time +from ..facades import Url as url from .optional import Optional as optional -from .structures import config, Dot, load -from .migrations import has_unmigrated_migrations -from masoniteorm.collection import Collection as collect +from .mix import MixHelper +from .urls import UrlsHelper +from .compact import Compact as compact diff --git a/src/masonite/helpers/compact.py b/src/masonite/helpers/compact.py new file mode 100644 index 000000000..ec8190285 --- /dev/null +++ b/src/masonite/helpers/compact.py @@ -0,0 +1,29 @@ +from ..exceptions import AmbiguousError +import inspect + + +class Compact: + def __new__(cls, *args): + frame = inspect.currentframe() + + cls.dictionary = {} + for arg in args: + if isinstance(arg, dict): + cls.dictionary.update(arg) + continue + + found = [] + for key, value in frame.f_back.f_locals.items(): + if value == arg: + for f in found: + if value is f and f is not None: + raise AmbiguousError( + "Cannot contain variables with multiple of the same object in scope. " + "Getting {}".format(value) + ) + cls.dictionary.update({key: value}) + found.append(value) + + if len(args) != len(cls.dictionary): + raise ValueError("Could not find all variables in this") + return cls.dictionary diff --git a/src/masonite/helpers/filesystem.py b/src/masonite/helpers/filesystem.py deleted file mode 100644 index 63c792584..000000000 --- a/src/masonite/helpers/filesystem.py +++ /dev/null @@ -1,31 +0,0 @@ -import os -import shutil - - -def make_directory(directory): - if not os.path.isfile(directory): - if not os.path.exists(os.path.dirname(directory)): - # Create the path to the model if it does not exist - os.makedirs(os.path.dirname(directory)) - - return True - - return False - - -def copy_migration(directory_file, to="databases/migrations"): - import datetime - - base_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../../") - - file_path = os.path.join(base_path, directory_file) - to_location = os.path.join( - os.getcwd(), - to, - datetime.datetime.utcnow().strftime("%Y_%m_%d_%H%M%S") - + "_" - + os.path.basename(directory_file), - ) - shutil.copyfile(file_path, to_location) - - print("\033[92m {} has been created \033[0m".format(to_location)) diff --git a/src/masonite/helpers/migrations.py b/src/masonite/helpers/migrations.py deleted file mode 100644 index f4fc10426..000000000 --- a/src/masonite/helpers/migrations.py +++ /dev/null @@ -1,43 +0,0 @@ -import subprocess - -from ..helpers import config, HasColoredCommands -from ..packages import add_venv_site_packages -from masoniteorm.migrations import Migration - - -class Migrations(HasColoredCommands): - def __init__(self, connection=None): - self._ran = [] - self._notes = [] - from config import database - - if not connection or connection == "default": - connection = database.DATABASES["default"] - self.migrator = Migration("sqlite") - self.migrator.create_table_if_not_exists() - - def run(self): - self.migrator.migrate() - - return self - - def rollback(self): - self.migrator.rollback() - - return self - - def refresh(self): - self.run() - self.rollback() - - def reset(self): - self.migrator.rollback_all() - - return self - - def ran(self): - return self._ran - - -def has_unmigrated_migrations(): - return False diff --git a/src/masonite/helpers/misc.py b/src/masonite/helpers/misc.py deleted file mode 100644 index 39d90fa42..000000000 --- a/src/masonite/helpers/misc.py +++ /dev/null @@ -1,121 +0,0 @@ -"""Module for miscellaneous helper methods.""" - -import random -import string -import warnings - -from ..exceptions import AmbiguousError - - -def random_string(length=4): - """Generate a random string based on the length given. - - Keyword Arguments: - length {int} -- The amount of the characters to generate (default: {4}) - - Returns: - string - """ - return "".join( - random.choice(string.ascii_uppercase + string.digits) for _ in range(length) - ) - - -def dot(data, compile_to=None): - notation_list = data.split(".") - - compiling = "" - compiling += notation_list[0] - beginning_string = compile_to.split("{1}")[0] - compiling = beginning_string + compiling - dot_split = compile_to.replace(beginning_string + "{1}", "").split("{.}") - if any(len(x) > 1 for x in dot_split): - raise ValueError("Cannot have multiple values between {1} and {.}") - - for notation in notation_list[1:]: - compiling += dot_split[0] - compiling += notation - compiling += dot_split[1] - return compiling - - -def clean_request_input(value, clean=True, quote=True): - if not clean: - return value - - import html - - try: - if isinstance(value, str): - return html.escape(value, quote=quote) - elif isinstance(value, list): - return [html.escape(x, quote=quote) for x in value] - elif isinstance(value, int): - return value - elif isinstance(value, dict): - return {key: html.escape(val, quote=quote) for (key, val) in value.items()} - except (AttributeError, TypeError): - pass - - return value - - -class HasColoredCommands: - def success(self, message): - print("\033[92m {0} \033[0m".format(message)) - - def warning(self, message): - print("\033[93m {0} \033[0m".format(message)) - - def danger(self, message): - print("\033[91m {0} \033[0m".format(message)) - - def info(self, message): - return self.success(message) - - -class Compact: - def __new__(cls, *args): - import inspect - - frame = inspect.currentframe() - - cls.dictionary = {} - for arg in args: - - if isinstance(arg, dict): - cls.dictionary.update(arg) - continue - - found = [] - for key, value in frame.f_back.f_locals.items(): - if value == arg: - for f in found: - if value is f and f is not None: - raise AmbiguousError( - "Cannot contain variables with multiple of the same object in scope. " - "Getting {}".format(value) - ) - cls.dictionary.update({key: value}) - found.append(value) - - if len(args) != len(cls.dictionary): - raise ValueError("Could not find all variables in this") - return cls.dictionary - - -def deprecated(message): - warnings.simplefilter("default", DeprecationWarning) - - def deprecated_decorator(func): - def deprecated_func(*args, **kwargs): - warnings.warn( - "{} is a deprecated function. {}".format(func.__name__, message), - category=DeprecationWarning, - stacklevel=2, - ) - return func(*args, **kwargs) - - return deprecated_func - - return deprecated_decorator diff --git a/src/masonite/helpers/mix.py b/src/masonite/helpers/mix.py new file mode 100644 index 000000000..ce8aba902 --- /dev/null +++ b/src/masonite/helpers/mix.py @@ -0,0 +1,34 @@ +import json +from os.path import join, exists + +from ..configuration import config +from ..utils.location import base_path +from ..exceptions import MixManifestNotFound, MixFileNotFound + + +class MixHelper: + def __init__(self, app): + self.app = app + + def url(self, path, manifest_dir=""): + if not path.startswith("/"): + path = "/" + path + + root_url = config("application.mix_base_url") or config("application.app_url") + + # load manifest file + manifest_file = base_path(join(manifest_dir, "mix-manifest.json")) + if not exists(manifest_file): + raise MixManifestNotFound( + "Mix manifest file mix-manifest.json does not exist." + ) + manifest = {} + with open(manifest_file, "r") as f: + manifest = json.load(f) + + # build asset path + try: + compiled_path = manifest[path] + except KeyError: + raise MixFileNotFound(f"Can't locate mix file: {path}.") + return join(root_url, compiled_path.lstrip("/")) diff --git a/src/masonite/helpers/optional.py b/src/masonite/helpers/optional.py index 4bf2d7e88..a39b29dfa 100644 --- a/src/masonite/helpers/optional.py +++ b/src/masonite/helpers/optional.py @@ -1,25 +1,35 @@ -class NoneType: +class DefaultType: + def __init__(self, value): + self.value = value + def __getattr__(self, attr): - return None + return self.value def __call__(self, *args, **kwargs): - return None + return self.value def __eq__(self, other): - return other is None + if self.value is None: + return other is self.value + else: + return other == self.value class Optional: - def __init__(self, obj): + """Optional helper class that allow evaluting an expression which can be undefined without + raising an expression but returning a default value (None).""" + + def __init__(self, obj, default=None): self.obj = obj + self.default = default def __getattr__(self, attr): if hasattr(self.obj, attr): return getattr(self.obj, attr) - return NoneType() + return DefaultType(self.default) def __call__(self, *args, **kwargs): - return NoneType() + return DefaultType(self.default) def instance(self): return self.obj diff --git a/src/masonite/helpers/password.py b/src/masonite/helpers/password.py deleted file mode 100644 index e361c4047..000000000 --- a/src/masonite/helpers/password.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Password Helper Module.""" - -import bcrypt - - -def password(password_string): - """Bcrypt a string. - - Useful for storing passwords in a database. - - Arguments: - pass {string} -- A string like a users plain text password to be bcrypted. - - Returns: - string -- The encrypted string. - """ - return bytes( - bcrypt.hashpw(bytes(password_string, "utf-8"), bcrypt.gensalt()) - ).decode("utf-8") diff --git a/src/masonite/helpers/routes.py b/src/masonite/helpers/routes.py deleted file mode 100644 index fd84b5e75..000000000 --- a/src/masonite/helpers/routes.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Helper Functions for RouteProvider.""" - -import re -from urllib.parse import parse_qs - -from .misc import deprecated - - -def flatten_routes(routes): - """Flatten the grouped routes into a single list of routes. - - Arguments: - routes {list} -- This can be a multi dementional list which can flatten all lists into a single list. - - Returns: - list -- Returns the flatten list. - """ - route_collection = [] - for route in routes: - if isinstance(route, list): - for r in flatten_routes(route): - route_collection.append(r) - else: - route_collection.append(route) - - return route_collection - - -def compile_route_to_regex(route): - """Compile a route to regex. - - Arguments: - route {masonite.routes.Route} -- The Masonite route object - - Returns: - string -- Returns the regex of the route. - """ - # Split the route - split_given_route = route.split("/") - - # compile the provided url into regex - url_list = [] - regex = "^" - for regex_route in split_given_route: - if "*" in regex_route or "@" in regex_route: - if ":int" in regex_route: - regex += r"(\d+)" - elif ":string" in regex_route: - regex += r"([a-zA-Z]+)" - else: - # default - regex += r"[\w.\-\/]+" - regex += r"\/" - - # append the variable name passed @(variable):int to a list - url_list.append( - regex_route.replace("@", "").replace(":int", "").replace(":string", "") - ) - else: - regex += regex_route + r"\/" - - if regex.endswith("/") and not route.endswith("/"): - regex = regex[:-2] - - regex += "$" - - return regex - - -def create_matchurl(url, route): - """Create a regex string for router.url to be matched against. - - Arguments: - router {masonite.routes.Route} -- The Masonite route object - route {masonite.routes.BaseHttpRoute} -- The current route being executed. - - Returns: - string -- compiled regex string - """ - - if route._compiled_regex is None: - route.compile_route_to_regex() - - if not url.endswith("/"): - return route._compiled_regex - elif url == "/": - return route._compiled_regex - - return route._compiled_regex_end - - -def query_parse(query_string): - d = {} - for key, value in parse_qs(query_string).items(): - regex_match = re.match(r"(?P [^\[]+)\[(?P [^\]]+)\]", key) - if regex_match: - gd = regex_match.groupdict() - d.setdefault(gd["key"], {})[gd["value"]] = value[0] - else: - d.update({key: value[0]}) - - return d diff --git a/src/masonite/helpers/sign.py b/src/masonite/helpers/sign.py deleted file mode 100644 index b44568af2..000000000 --- a/src/masonite/helpers/sign.py +++ /dev/null @@ -1,49 +0,0 @@ -from ..auth import Sign - - -def sign(value): - """Shortcut for Sign class. - - Arguments: - value {string} -- The value that is going to be encrypted - - Returns: - string -- The string value after encryption. - """ - return Sign().sign(value) - - -def encrypt(value): - """Shortcut for Sign class sign method. - - Arguments: - value {string} -- The value that is going to be encrypted - - Returns: - string -- The string value after encryption. - """ - return sign(value) - - -def unsign(value): - """Shortcut for Sign class unsign method. - - Arguments: - value {string} -- The value that is going to be decrypted - - Returns: - string -- The string value after decryption. - """ - return Sign().unsign(value) - - -def decrypt(value): - """Shortcut for Sign class unsign method. - - Arguments: - value {string} -- The value that is going to be decrypted - - Returns: - string -- The string value after decryption. - """ - return unsign(value) diff --git a/src/masonite/helpers/static.py b/src/masonite/helpers/static.py deleted file mode 100644 index fa13abd37..000000000 --- a/src/masonite/helpers/static.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Static Helper Module.""" - - -def static(alias, file_name): - """Get the static file location of an asset. - - Arguments: - alias {string} -- The driver and location to search for. This could be s3.uploads - file_name {string} -- The filename of the file to return. - - Returns: - string -- Returns the file location. - """ - from config.storage import DRIVERS - - if "." in alias: - alias = alias.split(".") - location = DRIVERS[alias[0]]["location"][alias[1]] - if location.endswith("/"): - location = location[:-1] - - return "{}/{}".format(location, file_name) - - location = DRIVERS[alias]["location"] - if isinstance(location, dict): - location = list(location.values())[0] - if location.endswith("/"): - location = location[:-1] - - return "{}/{}".format(location, file_name) diff --git a/src/masonite/helpers/structures.py b/src/masonite/helpers/structures.py deleted file mode 100644 index 548942efb..000000000 --- a/src/masonite/helpers/structures.py +++ /dev/null @@ -1,219 +0,0 @@ -"""A Module For Manipulating Code Structures.""" - -import inspect -import pydoc -from collections.abc import MutableMapping - -from masoniteorm.collection import Collection as collect - - -class Dot: - def dot(self, search, dictionary, default=None): - """The search string in dot notation to look into the dictionary for. - - Arguments: - search {string} -- This should be a string in dot notation - like 'key.key.value'. - dictionary {dict} -- A normal dictionary which will be searched using - the search string in dot notation. - - Keyword Arguments: - default {string} -- The default value if nothing is found - in the dictionary. (default: {None}) - - Returns: - string -- Returns the value found the dictionary or the default - value specified above if nothing is found. - """ - if "." not in search: - if search == "": - return dictionary - try: - return dictionary[search] - except KeyError: - return default - - searching = search.split(".") - possible = None - if "*" not in search: - return self.flatten(dictionary).get(search, default) - - while searching: - dic = dictionary - for value in searching: - if not dic: - if "*" in searching: - return [] - return default - - if isinstance(dic, list): - try: - return collect(dic).pluck(searching[searching.index("*") + 1]) - except KeyError: - return [] - - if not isinstance(dic, dict): - return default - - dic = dic.get(value) - - if isinstance(dic, str) and dic.isnumeric(): - continue - - if ( - dic - and not isinstance(dic, int) - and hasattr(dic, "__len__") - and len(dic) == 1 - and not isinstance(dic[list(dic)[0]], dict) - ): - possible = dic - - if not isinstance(dic, dict): - return dic - - del searching[-1] - return possible - - def flatten(self, d, parent_key="", sep="."): - items = [] - for k, v in d.items(): - new_key = parent_key + sep + k if parent_key else k - if isinstance(v, MutableMapping): - items.append((new_key, v)) - items.extend(self.flatten(v, new_key, sep=sep).items()) - elif isinstance(v, list): - for index, val in enumerate(v): - items.extend( - self.flatten({str(index): val}, new_key, sep=sep).items() - ) - else: - items.append((new_key, v)) - - return dict(items) - - def locate(self, search_path, default=""): - """Locate the object from the given search path - - Arguments: - search_path {string} -- A search path to fetch the object - from like config.application.debug. - - Keyword Arguments: - default {string} -- A default string if the search path is - not found (default: {''}) - - Returns: - any -- Could be a string, object or anything else that is fetched. - """ - value = self.find(search_path, default) - - if isinstance(value, dict): - return self.dict_dot(".".join(search_path.split(".")[3:]), value, default) - - if value is not None: - return value - - return default - - def dict_dot(self, search, dictionary, default=""): - """Takes a dot notation representation of a dictionary and fetches it from the dictionary. - - This will take something like s3.locations and look into the s3 dictionary and fetch the locations - key. - - Arguments: - search {string} -- The string to search for in the dictionary using dot notation. - dictionary {dict} -- The dictionary to search through. - - Returns: - string -- The value of the dictionary element. - """ - return self.dot(search, dictionary, default) - - def find(self, search_path, default=""): - """Used for finding both the uppercase and specified version. - - Arguments: - search_path {string} -- The search path to find the module, - dictionary key, object etc. This is typically - in the form of dot notation 'config.application.debug' - - Keyword Arguments: - default {string} -- The default value to return if the search path - could not be found. (default: {''}) - - Returns: - any -- Could be a string, object or anything else that is fetched. - """ - value = pydoc.locate(search_path) - - if value: - return value - - paths = search_path.split(".") - - value = pydoc.locate(".".join(paths[:-1]) + "." + paths[-1].upper()) - - if value or value is False: - return value - - search_path = -1 - - # Go backwards through the dot notation until a match is found. - ran = 0 - while ran < len(paths): - try: - value = pydoc.locate( - ".".join(paths[:search_path]) + "." + paths[search_path].upper() - ) - except IndexError: - return default - - if value: - break - - value = pydoc.locate( - ".".join(paths[:search_path]) + "." + paths[search_path] - ) - - if value: - break - - search_path -= 1 - ran += 1 - - if not value or inspect.ismodule(value): - return default - - return value - - -def config(path, default=""): - """Used to fetch a value from a configuration file - - Arguments: - path {string} -- The search path using dot notation of the value to get - - Keyword Arguments: - default {str} -- The default value if not value and be found (default: {''}) - - Returns: - mixed - """ - return Dot().locate("config." + path, default) - - -def load(path, default=""): - """Used to fetch a value from a configuration file - - Arguments: - path {string} -- The search path using dot notation of the value to get - - Keyword Arguments: - default {str} -- The default value if not value and be found (default: {''}) - - Returns: - mixed - """ - return Dot().locate(path, default) diff --git a/src/masonite/helpers/time.py b/src/masonite/helpers/time.py deleted file mode 100644 index aeacb4ee8..000000000 --- a/src/masonite/helpers/time.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Time Module.""" - -import pendulum - - -def cookie_expire_time(str_time): - """Take a string like 1 month or 5 minutes and returns a pendulum instance. - - Arguments: - str_time {string} -- Could be values like 1 second or 3 minutes - - Returns: - pendulum -- Returns Pendulum instance - """ - if str_time != "expired": - number = int(str_time.split(" ")[0]) - length = str_time.split(" ")[1] - - if length in ("second", "seconds"): - # Sat, 06 Jun 2020 15:36:16 GMT - return ( - pendulum.now("GMT") - .add(seconds=number) - .format("ddd, DD MMM YYYY H:mm:ss") - ) - elif length in ("minute", "minutes"): - return ( - pendulum.now("GMT") - .add(minutes=number) - .format("ddd, DD MMM YYYY H:mm:ss") - ) - elif length in ("hour", "hours"): - return ( - pendulum.now("GMT").add(hours=number).format("ddd, DD MMM YYYY H:mm:ss") - ) - elif length in ("days", "days"): - return ( - pendulum.now("GMT").add(days=number).format("ddd, DD MMM YYYY H:mm:ss") - ) - elif length in ("week", "weeks"): - return pendulum.now("GMT").add(weeks=1).format("ddd, DD MMM YYYY H:mm:ss") - elif length in ("month", "months"): - return ( - pendulum.now("GMT") - .add(months=number) - .format("ddd, DD MMM YYYY H:mm:ss") - ) - elif length in ("year", "years"): - return ( - pendulum.now("GMT").add(years=number).format("ddd, DD MMM YYYY H:mm:ss") - ) - - return None - else: - return pendulum.now("GMT").subtract(years=20).format("ddd, DD MMM YYYY H:mm:ss") - - -def parse_human_time(str_time): - """Take a string like 1 month or 5 minutes and returns a pendulum instance. - - Arguments: - str_time {string} -- Could be values like 1 second or 3 minutes - - Returns: - pendulum -- Returns Pendulum instance - """ - if str_time != "expired": - number = int(str_time.split(" ")[0]) - length = str_time.split(" ")[1] - - if length in ("second", "seconds"): - return pendulum.now("GMT").add(seconds=number) - elif length in ("minute", "minutes"): - return pendulum.now("GMT").add(minutes=number) - elif length in ("hour", "hours"): - return pendulum.now("GMT").add(hours=number) - elif length in ("days", "days"): - return pendulum.now("GMT").add(days=number) - elif length in ("week", "weeks"): - return pendulum.now("GMT").add(weeks=1) - elif length in ("month", "months"): - return pendulum.now("GMT").add(months=number) - elif length in ("year", "years"): - return pendulum.now("GMT").add(years=number) - - return None - else: - return pendulum.now("GMT").subtract(years=20) diff --git a/src/masonite/helpers/urls.py b/src/masonite/helpers/urls.py new file mode 100644 index 000000000..052f0fdfb --- /dev/null +++ b/src/masonite/helpers/urls.py @@ -0,0 +1,49 @@ +from os.path import join +from ..configuration import config + + +class UrlsHelper: + """URLs helper provide handy functions to build URLs.""" + + def __init__(self, app): + self.app = app + + def url(self, path=""): + """Generates a fully qualified url to the given path. If no path is given this will return + the base url domain.""" + # ensure that no slash is prefixing the relative path + relative_path = path.lstrip("/") + return join(config("application.app_url"), relative_path) + + def asset(self, alias, filename): + """Generates a fully qualified URL for the given asset using the given disk + Example: + asset("local", "avatar.jpg") (take first pat) + asset("s3.private", "doc.pdf") (when multiple paths are specified for the disk) + """ + disks = config("filesystem.disks") + # ensure that no slash is prefixing the relative filename path + filename = filename.lstrip("/") + if "." in alias: + alias = alias.split(".") + location = disks[alias[0]]["path"][alias[1]] + else: + location = disks[alias]["path"] + # take first path if no path specified + if isinstance(location, dict): + location = list(location.values())[0] + return join(location, filename) + + def route(self, name, params={}, absolute=True): + """Generates a fully qualified URL to the given route name. + Example: + route("users.home") : http://masonite.app/dashboard/ + route("users.profile", {"id": 1}) : http://masonite.app/users/1/profile/ + route("users.profile", {"id": 1}, absolute=False) : /users/1/profile/ + """ + + relative_url = self.app.make("router").route(name, params) + if absolute: + return self.url(relative_url) + else: + return relative_url diff --git a/src/masonite/helpers/view_helpers.py b/src/masonite/helpers/view_helpers.py deleted file mode 100644 index 4d8e41b1f..000000000 --- a/src/masonite/helpers/view_helpers.py +++ /dev/null @@ -1,62 +0,0 @@ -"""View Helper Module.""" - -from jinja2 import Markup - - -def set_request_method(method_type): - """Return an input string for use in a view to change the request method of a form. - - Arguments: - method_type {string} -- Can be options like GET, POST, PUT, PATCH, DELETE - - Returns: - string -- An input string. - """ - return Markup( - "".format(method_type) - ) - - -def back(location=None): - """Return an input element for use in telling Masonite which route to redirect back to. - - Arguments: - location {string} -- The route to redirect back to. - - Returns: - string -- An input string. - """ - if location is None: - from wsgi import container - - request = container.make("Request") - intended_route = request.session.get("__intend") - if intended_route: - location = intended_route - else: - location = request.path - - return Markup("".format(location)) - - -def hidden(value, name="hidden-input"): - return Markup("".format(name, value)) - - -def old(session_key, default=""): - """Return the old value submitted by forms validated with validators. - - Arguments: - session_key {string} -- The key flashed to session. - - Returns: - string -- An input string. - """ - - from wsgi import container - - session_container = container.make("Session") - - if session_container.has(session_key): - return session_container.get_flashed(session_key) - return default diff --git a/src/masonite/hook.py b/src/masonite/hook.py deleted file mode 100644 index 5b1ec4ba4..000000000 --- a/src/masonite/hook.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Module for the Hook class.""" - -from .app import App - - -class Hook: - """Hook class is responsible for finding and firing framework hooks.""" - - def __init__(self, app: App): - """Hook constructor. - - Arguments: - app {masonite.app.App} -- Container object. - """ - self._app = app - - def fire(self, search): - """Find all the classes to be fired with the exception hook search string. - - Arguments: - search {string} -- The search string to collect classes with. - """ - for key in self._app.collect(search): - self._app.make(key).load(self._app) diff --git a/src/masonite/input/Input.py b/src/masonite/input/Input.py new file mode 100644 index 000000000..d2aa2e244 --- /dev/null +++ b/src/masonite/input/Input.py @@ -0,0 +1,4 @@ +class Input: + def __init__(self, name, value): + self.name = name + self.value = value diff --git a/src/masonite/input/InputBag.py b/src/masonite/input/InputBag.py index ccc4a54e4..1f036dade 100644 --- a/src/masonite/input/InputBag.py +++ b/src/masonite/input/InputBag.py @@ -1,10 +1,11 @@ from .Input import Input from urllib.parse import parse_qs -import email.parser +import re import json import cgi import re -from ..helpers import Dot as DictDot, clean_request_input +from ..utils.structures import data_get +from ..filesystem import UploadedFile class InputBag: @@ -22,8 +23,8 @@ def load(self, environ): def parse(self, environ): if "QUERY_STRING" in environ: - self.query_string = self.query_parse(environ["QUERY_STRING"]) + if "wsgi.input" in environ: if "application/json" in environ.get("CONTENT_TYPE", ""): try: @@ -32,6 +33,7 @@ def parse(self, environ): request_body_size = 0 request_body = environ["wsgi.input"].read(request_body_size) + if isinstance(request_body, bytes): request_body = request_body.decode("utf-8") @@ -41,6 +43,7 @@ def parse(self, environ): else: for name, value in json.loads(request_body or "{}").items(): self.post_data.update({name: Input(name, value)}) + elif "application/x-www-form-urlencoded" in environ.get("CONTENT_TYPE", ""): try: request_body_size = int(environ.get("CONTENT_LENGTH", 0)) @@ -48,12 +51,10 @@ def parse(self, environ): request_body_size = 0 request_body = environ["wsgi.input"].read(request_body_size) - if isinstance(request_body, bytes): - request_body = request_body.decode("utf-8") + parsed_request_body = parse_qs(bytes(request_body).decode("utf-8")) + + self.post_data = self.parse_dict(parsed_request_body) - for parts in request_body.split("&"): - name, value = parts.split("=", 1) - self.post_data.update({name: Input(name, value)}) elif "multipart/form-data" in environ.get("CONTENT_TYPE", ""): try: request_body_size = int(environ.get("CONTENT_LENGTH", 0)) @@ -67,7 +68,21 @@ def parse(self, environ): ) for name in fields: - self.post_data.update({name: Input(name, fields.getvalue(name))}) + value = fields.getvalue(name) + if isinstance(value, bytes): + self.post_data.update( + { + name: UploadedFile( + fields[name].filename, fields.getvalue(name) + ) + } + ) + else: + self.post_data.update( + {name: Input(name, fields.getvalue(name))} + ) + + self.post_data = self.parse_dict(self.post_data) else: try: request_body_size = int(environ.get("CONTENT_LENGTH", 0)) @@ -75,18 +90,33 @@ def parse(self, environ): request_body_size = 0 request_body = environ["wsgi.input"].read(request_body_size) + if request_body: + self.post_data.update( + json.loads(bytes(request_body).decode("utf-8")) + ) - def get(self, name, default=None): + def get(self, name, default=None, clean=True, quote=True): + input = data_get(self.all(), name, default) - input = DictDot().dot(name, self.all(), default=default) - if isinstance(input, (dict, str)): + if isinstance(input, (str,)): return input + if isinstance(input, list) and len(input) == 1: + return input[0] + elif isinstance(input, (dict,)): + rendered = {} + for key, inp in input.items(): + if hasattr(inp, "value"): + inp = inp.value + rendered.update({key: inp}) + return rendered elif hasattr(input, "value"): + if isinstance(input.value, list) and len(input.value) == 1: + return input.value[0] + elif isinstance(input.value, dict): + return input.value return input.value - else: - return input - return default + return input def has(self, *names): return all((name in self.all()) for name in names) @@ -112,14 +142,54 @@ def all_as_values(self, internal_variables=False): return new + def only(self, *args): + all = self.all() + new = {} + for name, input in all.items(): + if name not in args: + continue + new.update({name: self.get(name)}) + + return new + def query_parse(self, query_string): + return self.parse_dict(parse_qs(query_string)) + + def parse_dict(self, dictionary): d = {} - for name, value in parse_qs(query_string).items(): - regex_match = re.match(r"(?P [^\[]+)\[(?P [^\]]+)\]", name) - if regex_match: - gd = regex_match.groupdict() - d.setdefault(gd["name"], {})[gd["value"]] = Input(name, value[0]) + for name, value in dictionary.items(): + if name.endswith("[]"): + d.update({name: value}) + else: + regex_match = re.match(r"(?P [^\[]+)\[(?P [^\]]+)\]", name) + + if regex_match: + gd = regex_match.groupdict() + if isinstance(value, Input): + d.setdefault(gd["name"], {})[gd["value"]] = value + else: + d.setdefault(gd["name"], {})[gd["value"]] = value[0] + else: + try: + d.update({name: value[0]}) + except TypeError: + d.update({name: value}) + + new_dict = {} + # Further filter the dictionary + for name, value in d.items(): + if "[]" in name: + new_name = name.replace("[]", "") + regex_match = re.match( + r"(?P [^\[]+)*\[(?P [^\]]+)\]", new_name + ) + if regex_match: + new_dict.setdefault(regex_match["name"], []).append( + {regex_match["value"]: value} + ) + else: + new_dict.update({name: value}) else: - d.update({name: Input(name, value[0])}) + new_dict.update({name: value}) - return d + return new_dict diff --git a/src/masonite/input/__init__.py b/src/masonite/input/__init__.py new file mode 100644 index 000000000..fb3f53e79 --- /dev/null +++ b/src/masonite/input/__init__.py @@ -0,0 +1 @@ +from .InputBag import InputBag diff --git a/src/masonite/listeners/BaseExceptionListener.py b/src/masonite/listeners/BaseExceptionListener.py deleted file mode 100644 index 8220c34e1..000000000 --- a/src/masonite/listeners/BaseExceptionListener.py +++ /dev/null @@ -1,2 +0,0 @@ -class BaseExceptionListener: - pass diff --git a/src/masonite/listeners/__init__.py b/src/masonite/listeners/__init__.py deleted file mode 100644 index 9446b2ad1..000000000 --- a/src/masonite/listeners/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .BaseExceptionListener import BaseExceptionListener diff --git a/src/masonite/loader/Loader.py b/src/masonite/loader/Loader.py new file mode 100644 index 000000000..cc1909dbd --- /dev/null +++ b/src/masonite/loader/Loader.py @@ -0,0 +1,78 @@ +"""Loader class to easily list, find or load any object in a given module, or folder.""" +import inspect +import pkgutil +import os + +from ..exceptions import LoaderNotFound +from ..utils.str import as_filepath +from ..utils.structures import load + + +def parameters_filter(obj_name, obj): + return ( + obj_name.isupper() + and not obj_name.startswith("__") + and not obj_name.endswith("__") + ) + + +class Loader: + def get_modules(self, files_or_directories, raise_exception=False): + if not isinstance(files_or_directories, list): + files_or_directories = [files_or_directories] + + _modules = {} + module_paths = list(map(as_filepath, files_or_directories)) + for module_loader, name, _ in pkgutil.iter_modules(module_paths): + module = load( + f"{os.path.relpath(module_loader.path)}.{name}", + raise_exception=raise_exception, + ) + _modules.update({name: module}) + return _modules + + def find(self, class_instance, paths, class_name, raise_exception=False): + _classes = self.find_all(class_instance, paths) + for name, obj in _classes.items(): + if name == class_name: + return obj + if raise_exception: + raise LoaderNotFound( + f"No {class_instance} named {class_name} has been found in {paths}" + ) + return None + + def find_all(self, class_instance, paths, raise_exception=False): + _classes = {} + for module in self.get_modules(paths).values(): + for obj_name, obj in inspect.getmembers(module): + # check if obj is the same class as the given one + if inspect.isclass(obj) and issubclass(obj, class_instance): + # check if the class really belongs to those paths to load internal only + if obj.__module__.startswith(module.__package__): + _classes.update({obj_name: obj}) + if not len(_classes.keys()) and raise_exception: + raise LoaderNotFound(f"No {class_instance} have been found in {paths}") + return _classes + + def get_object(self, path_or_module, object_name, raise_exception=False): + return load(path_or_module, object_name, raise_exception=raise_exception) + + def get_objects(self, path_or_module, filter_method=None, raise_exception=False): + """Returns a dictionary of objects from the given path (file or dotted). The dictionary can + be filtered if a given callable is given.""" + if isinstance(path_or_module, str): + module = load(path_or_module, raise_exception=raise_exception) + else: + module = path_or_module + if not module: + return None + return dict(inspect.getmembers(module, filter_method)) + + def get_parameters(self, module_or_path): + _parameters = {} + for name, obj in self.get_objects(module_or_path).items(): + if parameters_filter(name, obj): + _parameters.update({name: obj}) + + return _parameters diff --git a/src/masonite/loader/__init__.py b/src/masonite/loader/__init__.py new file mode 100644 index 000000000..b66f1da04 --- /dev/null +++ b/src/masonite/loader/__init__.py @@ -0,0 +1 @@ +from .Loader import Loader diff --git a/src/masonite/mail/Mail.py b/src/masonite/mail/Mail.py new file mode 100644 index 000000000..63f459f19 --- /dev/null +++ b/src/masonite/mail/Mail.py @@ -0,0 +1,33 @@ +class Mail: + def __init__(self, application, driver_config=None): + self.application = application + self.drivers = {} + self.driver_config = driver_config or {} + self.options = {} + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + + def set_configuration(self, config): + self.driver_config = config + return self + + def get_driver(self, name=None): + if name is None: + return self.drivers[self.driver_config.get("default")] + return self.drivers[name] + + def get_config_options(self, driver=None): + if driver is None: + return self.driver_config.get(self.driver_config.get("default"), {}) + + return self.driver_config.get(driver, {}) + + def mailable(self, mailable): + self.options = mailable.set_application(self.application).build().get_options() + return self + + def send(self, driver=None): + selected_driver = driver or self.options.get("driver", None) + self.options.update(self.get_config_options(selected_driver)) + return self.get_driver(selected_driver).set_options(self.options).send() diff --git a/src/masonite/mail/Mailable.py b/src/masonite/mail/Mailable.py new file mode 100644 index 000000000..d99e44349 --- /dev/null +++ b/src/masonite/mail/Mailable.py @@ -0,0 +1,102 @@ +from .MessageAttachment import MessageAttachment + + +class Mailable: + def __init__(self): + self._to = "" + self._cc = "" + self._bcc = "" + self._from = "" + self._reply_to = "" + self._subject = "" + self._priority = None + self._driver = None + self.text_content = "" + self.html_content = "" + self.attachments = [] + + def to(self, to): + self._to = to + return self + + def cc(self, cc): + self._cc = cc + return self + + def bcc(self, bcc): + self._bcc = bcc + return self + + def set_application(self, application): + self.application = application + return self + + def from_(self, _from): + self._from = _from + return self + + def attach(self, name, path): + self.attachments.append(MessageAttachment(name, path)) + return self + + def reply_to(self, reply_to): + self._reply_to = reply_to + return self + + def subject(self, subject): + self._subject = subject + return self + + def text(self, content): + self.text_content = content + return self + + def html(self, content): + self.html_content = content + return self + + def view(self, view, data={}): + return self.html( + self.application.make("view").render(view, data).rendered_template + ) + + def priority(self, priority): + self._priority = str(priority) + return self + + def high_priority(self): + self.priority(1) + return self + + def low_priority(self): + self.priority(5) + return self + + def driver(self, driver): + self._driver = driver + return self + + def get_response(self): + self.build() + if self.get_options().get("html_content"): + return self.get_options().get("html_content") + if self.get_options().get("text_content"): + return self.get_options().get("text_content") + + def get_options(self): + return { + "to": self._to, + "cc": self._cc, + "bcc": self._bcc, + "from": self._from, + "subject": self._subject, + "text_content": self.text_content, + "html_content": self.html_content, + "reply_to": self._reply_to, + "attachments": self.attachments, + "priority": self._priority, + "driver": self._driver, + } + + def build(self, *args, **kwargs): + return self diff --git a/src/masonite/mail/MessageAttachment.py b/src/masonite/mail/MessageAttachment.py new file mode 100644 index 000000000..0f3cb1cab --- /dev/null +++ b/src/masonite/mail/MessageAttachment.py @@ -0,0 +1,4 @@ +class MessageAttachment: + def __init__(self, alias, path): + self.alias = alias + self.path = path diff --git a/src/masonite/mail/MockMail.py b/src/masonite/mail/MockMail.py new file mode 100644 index 000000000..dcf8ef890 --- /dev/null +++ b/src/masonite/mail/MockMail.py @@ -0,0 +1,101 @@ +from .Mail import Mail + + +class MockMail(Mail): + def __init__(self, application, *args, **kwargs): + super().__init__(application, *args, **kwargs) + self.count = 0 + + def send(self, driver=None): + self.count += 1 + return self + + def seeEmailBcc(self, bcc): + assert bcc == self.options.get( + "bcc" + ), f"BCC of {self.options.get('bcc')} does not match expected {bcc}" + return self + + def seeEmailCc(self, cc): + assert cc == self.options.get( + "cc" + ), f"CC of {self.options.get('cc')} does not match expected {cc}" + return self + + def seeEmailContains(self, contents): + assert contents in self.options.get( + "html_content" + ) or contents in self.options.get( + "text_content" + ), f"Could not find the {contents} in the email" + return self + + def getHtmlContents(self, contents): + return self.options.get("html_content") + + def getTextContents(self, contents): + return self.options.get("text_content") + + def seeEmailCountEquals(self, count): + assert ( + count == self.count + ), f"Email count of {self.count} does not match expected {count}" + return self + + def seeEmailDoesNotContain(self, contents): + assert contents not in self.options.get( + "html_content" + ) and contents not in self.options.get( + "text_content" + ), f"Found {contents} in the email but should not be" + return self + + def seeEmailFrom(self, assertion): + assert assertion == self.options.get( + "from" + ), f"Assertion of from address {self.options.get('from')} does not match expected {assertion}" + return self + + def seeEmailReplyTo(self, assertion): + assert assertion == self.options.get( + "reply_to" + ), f"Assertion of reply-to {self.options.get('reply_to')} does not match expected {assertion}" + return self + + def seeEmailSubjectContains(self, assertion): + assert assertion in self.options.get( + "subject" + ), f"Assertion of subject {self.options.get('subject')} does not contain expected {assertion}" + return self + + def seeEmailSubjectDoesNotContain(self, assertion): + assert assertion not in self.options.get( + "subject" + ), f"Assertion of subject {self.options.get('subject')} does contain expected {assertion}" + return self + + def seeEmailSubjectEquals(self, assertion): + assert assertion == self.options.get( + "subject" + ), f"Assertion of subject address {self.options.get('subject')} does not match expected {assertion}" + return self + + def seeEmailTo(self, assertion): + assert assertion == self.options.get( + "to" + ), f"Assertion of to address {self.options.get('to')} does not match expected {assertion}" + return self + + def seeEmailPriority(self, assertion): + assert assertion == self.options.get( + "priority" + ), f"Assertion of priority {self.options.get('priority')} does not match expected {assertion}" + return self + + def seeEmailWasNotSent(self): + assert self.count == 0, "Expected email was not sent but it was sent" + return self + + def seeEmailWasSent(self): + assert self.count > 0, "Expected email was not sent but it was sent" + return self diff --git a/src/masonite/mail/Recipient.py b/src/masonite/mail/Recipient.py new file mode 100644 index 000000000..29e0dabb7 --- /dev/null +++ b/src/masonite/mail/Recipient.py @@ -0,0 +1,18 @@ +class Recipient: + def __init__(self, recipient): + if isinstance(recipient, (list, tuple)): + recipient = ",".join(recipient) + self.recipient = recipient + + def header(self): + headers = [] + for address in self.recipient.split(","): + + if "<" in address: + headers.append(address) + continue + + if address.strip(): + headers.append(f"<{address.strip()}>") + + return ", ".join(headers) diff --git a/src/masonite/mail/__init__.py b/src/masonite/mail/__init__.py new file mode 100644 index 000000000..ae10d6645 --- /dev/null +++ b/src/masonite/mail/__init__.py @@ -0,0 +1,3 @@ +from .Mail import Mail +from .Mailable import Mailable +from .MockMail import MockMail diff --git a/src/masonite/mail/drivers/MailgunDriver.py b/src/masonite/mail/drivers/MailgunDriver.py new file mode 100644 index 000000000..57d8a1fb6 --- /dev/null +++ b/src/masonite/mail/drivers/MailgunDriver.py @@ -0,0 +1,51 @@ +import requests +from ..Recipient import Recipient + + +class MailgunDriver: + def __init__(self, application): + self.application = application + self.options = {} + self.content_type = None + + def set_options(self, options): + self.options = options + return self + + def get_mime_message(self): + data = { + "from": self.options.get("from"), + "to": Recipient(self.options.get("to")).header(), + "subject": self.options.get("subject"), + "h:Reply-To": self.options.get("reply_to"), + "html": self.options.get("html_content"), + "text": self.options.get("text_content"), + } + + if self.options.get("cc"): + data.update({"cc", self.options.get("cc")}) + if self.options.get("bcc"): + data.update({"bcc", self.options.get("bcc")}) + if self.options.get("priority"): + data.update({"h:X-Priority", self.options.get("priority")}) + + return data + + def get_attachments(self): + files = [] + for attachment in self.options.get("attachments", []): + files.append(("attachment", open(attachment.path, "rb"))) + + return files + + def send(self): + domain = self.options["domain"] + secret = self.options["secret"] + attachments = self.get_attachments() + + return requests.post( + f"https://api.mailgun.net/v3/{domain}/messages", + auth=("api", secret), + data=self.get_mime_message(), + files=attachments, + ) diff --git a/src/masonite/mail/drivers/SMTPDriver.py b/src/masonite/mail/drivers/SMTPDriver.py new file mode 100644 index 000000000..bd6b3052f --- /dev/null +++ b/src/masonite/mail/drivers/SMTPDriver.py @@ -0,0 +1,76 @@ +import smtplib +from email.mime.multipart import MIMEMultipart +from email.mime.application import MIMEApplication +from email.mime.text import MIMEText +from ..Recipient import Recipient +import ssl + + +class SMTPDriver: + def __init__(self, application): + self.application = application + self.options = {} + + def set_options(self, options): + self.options = options + return self + + def get_mime_message(self): + message = MIMEMultipart("alternative") + + message["Subject"] = self.options.get("subject") + + message["From"] = Recipient(self.options.get("from")).header() + message["To"] = Recipient(self.options.get("to")).header() + if self.options.get("reply_to"): + message["Reply-To"] = Recipient(self.options.get("reply_to")).header() + + if self.options.get("cc"): + message["Cc"] = Recipient(self.options.get("cc")).header() + + if self.options.get("bcc"): + message["Bcc"] = Recipient(self.options.get("bcc")).header() + + if self.options.get("html_content"): + message.attach(MIMEText(self.options.get("html_content"), "html")) + + if self.options.get("text_content"): + message.attach(MIMEText(self.options.get("text_content"), "plain")) + + if self.options.get("priority"): + message["X-Priority"] = self.options.get("priority") + + for attachment in self.options.get("attachments", []): + with open(attachment.path, "rb") as fil: + part = MIMEApplication(fil.read(), Name=attachment.alias) + + part["Content-Disposition"] = f"attachment; filename={attachment.alias}" + message.attach(part) + + return message + + def make_connection(self): + options = self.options + if options.get("ssl"): + smtp = smtplib.SMTP_SSL("{0}:{1}".format(options["host"], options["port"])) + else: + smtp = smtplib.SMTP("{0}:{1}".format(options["host"], int(options["port"]))) + + if options.get("tls"): + context = ssl.create_default_context() + context.check_hostname = False + + # Check if correct response code for starttls is received from the server + if smtp.starttls(context=context)[0] != 220: + raise smtplib.SMTPNotSupportedError( + "Server is using untrusted protocol." + ) + + if options.get("username") and options.get("password"): + smtp.login(options.get("username"), options.get("password")) + + return smtp + + def send(self): + smtp = self.make_connection() + smtp.send_message(self.get_mime_message()) diff --git a/src/masonite/mail/drivers/TerminalDriver.py b/src/masonite/mail/drivers/TerminalDriver.py new file mode 100644 index 000000000..d519e3b7b --- /dev/null +++ b/src/masonite/mail/drivers/TerminalDriver.py @@ -0,0 +1,30 @@ +from ..Recipient import Recipient + + +class TerminalDriver: + def __init__(self, application): + self.application = application + self.options = {} + self.content_type = None + + def set_options(self, options): + self.options = options + return self + + def send(self): + print("-------------------------------------") + print(f"To: {Recipient(self.options.get('to')).header()}") + print(f"From: {Recipient(self.options.get('from')).header()}") + print(f"Cc: {Recipient(self.options.get('cc')).header()}") + print(f"Bcc: {Recipient(self.options.get('bcc')).header()}") + print(f"Subject: {self.options.get('subject')}") + print("-------------------------------------") + print(f"{self.options.get('html_content')}") + if self.options.get("text_content"): + print("-------------------------------------") + print(f"Text Content: {self.options.get('text_content')}") + if self.options.get("attachments"): + print("-------------------------------------") + for index, attachment in enumerate(self.options.get("attachments")): + index += 1 + print(f"Attachment {index}: {attachment.alias} from {attachment.path}") diff --git a/src/masonite/mail/drivers/__init__.py b/src/masonite/mail/drivers/__init__.py new file mode 100644 index 000000000..ac606aeab --- /dev/null +++ b/src/masonite/mail/drivers/__init__.py @@ -0,0 +1,3 @@ +from .SMTPDriver import SMTPDriver +from .MailgunDriver import MailgunDriver +from .TerminalDriver import TerminalDriver diff --git a/src/masonite/managers/AuthManager.py b/src/masonite/managers/AuthManager.py deleted file mode 100644 index 483db9e11..000000000 --- a/src/masonite/managers/AuthManager.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Auth Manager Module.""" - -from .Manager import Manager - - -class AuthManager(Manager): - """Manages all auth drivers. - - Arguments: - Manager {from .managers.Manager} -- The base Manager class. - """ - - config = "auth" - driver_prefix = "Auth" - - -class Auth: - """Dummy class that will be used to swap out the manager in the container.""" - - pass diff --git a/src/masonite/managers/BroadcastManager.py b/src/masonite/managers/BroadcastManager.py deleted file mode 100644 index 460a82883..000000000 --- a/src/masonite/managers/BroadcastManager.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Broadcast Manager Module.""" - -from ..contracts import BroadcastManagerContract -from .Manager import Manager - - -class BroadcastManager(Manager, BroadcastManagerContract): - """Manages all broadcast drivers. - - Arguments: - Manager {from .managers.Manager} -- The base Manager class. - """ - - config = "broadcast" - driver_prefix = "Broadcast" - - -class Broadcast: - """Dummy class that will be used to swap out the manager in the container.""" - - pass diff --git a/src/masonite/managers/CacheManager.py b/src/masonite/managers/CacheManager.py deleted file mode 100644 index 28da99fcc..000000000 --- a/src/masonite/managers/CacheManager.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Cache Manager.""" - -from ..contracts import CacheManagerContract -from .Manager import Manager - - -class CacheManager(Manager, CacheManagerContract): - """Manages all cache drivers. - - Arguments: - Manager {from .managers.Manager} -- The base Manager class. - """ - - config = "cache" - driver_prefix = "Cache" - - -class Cache: - """Dummy class that will be used to swap out the manager in the container.""" - - pass diff --git a/src/masonite/managers/MailManager.py b/src/masonite/managers/MailManager.py deleted file mode 100644 index 5382e4402..000000000 --- a/src/masonite/managers/MailManager.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Mail Manager Module.""" - -from ..contracts import MailManagerContract -from .Manager import Manager -from ..helpers import config - - -class MailManager(Manager, MailManagerContract): - """Manages all mail drivers. - - Arguments: - Manager {from .managers.Manager} -- The base Manager class. - """ - - config = "mail" - driver_prefix = "Mail" - - def helper(self): - """Helper Method to work with returning the driver from the MailManager. - - Returns: - Mail Driver - """ - return self.driver(config("mail.driver")) - - -class Mail: - """Dummy class that will be used to swap out the manager in the container.""" - - pass diff --git a/src/masonite/managers/Manager.py b/src/masonite/managers/Manager.py deleted file mode 100644 index 4d5b01478..000000000 --- a/src/masonite/managers/Manager.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Manager Module.""" - -import inspect - -from ..exceptions import ( - DriverNotFound, - MissingContainerBindingNotFound, - UnacceptableDriverType, -) - -from ..app import App -from ..helpers import config - - -class Manager: - """Base Manager Class.""" - - config = None - driver_prefix = None - - def __init__(self, container: App): - """Manager constructor. - - Keyword Arguments: - container {masonite.app.App} -- The container class (default: {None}) - """ - self.manage_driver = None - self.container = container - - def load_container(self, container): - """Load the container into the class and creates the default driver. - - Arguments: - container {masonite.app.App} -- The container class - - Returns: - self - """ - self.container = container - self.create_driver() - return self - - def driver(self, driver): - """Create the driver specified and returns the driver instance. - - Arguments: - driver {masonite.drivers.Driver} -- An instance of a Driver class. - - Returns: - masonite.drivers.Driver -- Returns a driver which is an instance of the base Driver class. - """ - self.create_driver(driver) - return self.manage_driver.load_manager(self) - - def create_driver(self, driver=None): - """Create the driver to be used. - - This could be used as the default driver when the manager is created or called internally on the fly - to change to a specific driver - - Keyword Arguments: - driver {string} -- The name of the driver to switch to (default: {None}) - - Raises: - UnacceptableDriverType -- Raised when a driver passed in is not a string or a class - DriverNotFound -- Raised when the driver can not be found. - """ - - if driver in (None, "default"): - driver = config("{}.driver".format(self.config)).capitalize() - else: - if isinstance(driver, str): - driver = driver.capitalize() - - try: - if isinstance(driver, str): - self.manage_driver = self.container.make( - "{0}{1}Driver".format(self.driver_prefix, driver) - ) - return - elif inspect.isclass(driver): - self.manage_driver = self.container.resolve(driver) - return - - raise UnacceptableDriverType( - "String or class based driver required. {} driver recieved.".format( - driver - ) - ) - except MissingContainerBindingNotFound: - raise DriverNotFound( - "Could not find the {0}{1}Driver from the service container. Are you missing a service provider?".format( - self.driver_prefix, driver - ) - ) diff --git a/src/masonite/managers/QueueManager.py b/src/masonite/managers/QueueManager.py deleted file mode 100644 index 0dfd11a73..000000000 --- a/src/masonite/managers/QueueManager.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Queue Manager Module.""" - -from ..contracts import QueueManagerContract -from .Manager import Manager - - -class QueueManager(Manager, QueueManagerContract): - """Manages all queue drivers. - - Arguments: - Manager {from .managers.Manager} -- The base Manager class. - """ - - config = "queue" - driver_prefix = "Queue" - - -class Queue: - """Dummy class that will be used to swap out the manager in the container.""" - - pass diff --git a/src/masonite/managers/SessionManager.py b/src/masonite/managers/SessionManager.py deleted file mode 100644 index 98a9f55b1..000000000 --- a/src/masonite/managers/SessionManager.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Session Manager Module.""" - -from ..contracts import SessionManagerContract -from .Manager import Manager - - -class SessionManager(Manager, SessionManagerContract): - """Manages all session drivers. - - Arguments: - Manager {from .managers.Manager} -- The base Manager class. - """ - - config = "session" - driver_prefix = "Session" - - -class Session: - """Dummy class that will be used to swap out the manager in the container.""" - - pass diff --git a/src/masonite/managers/StorageManager.py b/src/masonite/managers/StorageManager.py deleted file mode 100644 index 52095654b..000000000 --- a/src/masonite/managers/StorageManager.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Queue Manager Module.""" - -from ..contracts import StorageManagerContract -from .Manager import Manager - - -class StorageManager(Manager, StorageManagerContract): - """Manages all queue drivers. - - Arguments: - Manager {from .managers.Manager} -- The base Manager class. - """ - - config = "storage" - driver_prefix = "Storage" - - -class Storage: - """Dummy class that will be used to swap out the manager in the container.""" - - pass diff --git a/src/masonite/managers/UploadManager.py b/src/masonite/managers/UploadManager.py deleted file mode 100644 index 39ba1851e..000000000 --- a/src/masonite/managers/UploadManager.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Upload Manager Module.""" - -from ..contracts import UploadManagerContract -from .Manager import Manager - - -class UploadManager(Manager, UploadManagerContract): - """Manages all upload drivers. - - Arguments: - Manager {from .managers.Manager} -- The base Manager class. - """ - - config = "storage" - driver_prefix = "Upload" - - -class Upload: - """Dummy class that will be used to swap out the manager in the container.""" - - pass diff --git a/src/masonite/managers/__init__.py b/src/masonite/managers/__init__.py deleted file mode 100644 index ed8d49a29..000000000 --- a/src/masonite/managers/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .Manager import Manager -from .AuthManager import AuthManager -from .BroadcastManager import BroadcastManager -from .CacheManager import CacheManager -from .MailManager import MailManager -from .QueueManager import QueueManager -from .SessionManager import SessionManager -from .StorageManager import StorageManager -from .UploadManager import UploadManager diff --git a/src/masonite/middleware/CorsMiddleware.py b/src/masonite/middleware/CorsMiddleware.py deleted file mode 100644 index ddfdf0acd..000000000 --- a/src/masonite/middleware/CorsMiddleware.py +++ /dev/null @@ -1,25 +0,0 @@ -"""CORS Middleware.""" - -from ..helpers import config -from ..response import Response - - -class CorsMiddleware: - """Appends CORS headers to HTTP response. - - Put any CORS middleware you need as a CORS dictionary inside your - middleware config file. - """ - - def __init__(self, response: Response): - """Inject Any Dependencies From The Service Container. - - Arguments: - Request {masonite.request.Request} -- The Masonite request object - """ - self.response = response - - def before(self): - """Run This Middleware After The Route Executes.""" - headers = config("middleware.cors") or {} - self.response.header(headers) diff --git a/src/masonite/middleware/CsrfMiddleware.py b/src/masonite/middleware/CsrfMiddleware.py deleted file mode 100644 index 7bf7cdebc..000000000 --- a/src/masonite/middleware/CsrfMiddleware.py +++ /dev/null @@ -1,95 +0,0 @@ -"""CSRF Middleware.""" - -from jinja2 import Markup - -from ..auth import Csrf -from ..exceptions import InvalidCSRFToken -from ..request import Request -from ..view import View -import binascii -import os - - -class CsrfMiddleware: - """Verify CSRF Token Middleware.""" - - exempt = ["/"] - every_request = True - token_length = 30 - - def __init__(self, request: Request, csrf: Csrf, view: View): - """Initialize the CSRF Middleware - - Arguments: - request {masonite.request.Request} -- The normal Masonite request class. - csrf {masonite.auth.Csrf} -- CSRF auth class. - view {masonite.view.View} -- The normal Masonite view class. - """ - - self.request = request - self.csrf = csrf - self.view = view - - def before(self): - """Execute this method before the controller.""" - if not self.request.get_cookie("MSESSID"): - session_id = bytes( - binascii.b2a_hex(os.urandom(self.token_length // 2)) - ).decode("utf-8") - self.request.cookie("MSESSID", session_id, expires="5 minutes") - token = self.verify_token() - - self.view.share( - { - "csrf_field": Markup( - "".format(token) - ), - "csrf_token": token, - } - ) - - def after(self): - pass - - def in_exempt(self): - """Determine if the request has a URI that should pass through CSRF verification. - - Returns: - bool - """ - for route in self.exempt: - if self.request.contains(route): - return True - - return False - - def generate_token(self): - """Generate a token that will be used for CSRF protection - - Returns: - string -- A random string based on the length given - """ - - return self.csrf.generate_csrf_token(self.token_length) - - def verify_token(self): - """Verify if csrf token in post is valid. - - Raises: - InvalidCSRFToken -- Thrown if the CSRF tokens do not match. - - Returns: - string -- Returns a new token or the current token. - """ - if self.request.is_not_safe() and not self.in_exempt(): - token = ( - self.request.header("X-CSRF-TOKEN") - or self.request.header("X-XSRF-TOKEN") - or self.request.input("__token") - ) - if not self.csrf.verify_csrf_token(token): - raise InvalidCSRFToken("Invalid CSRF token.") - - return token - else: - return self.generate_token() diff --git a/src/masonite/middleware/GuardMiddleware.py b/src/masonite/middleware/GuardMiddleware.py deleted file mode 100644 index 0eb6d9e52..000000000 --- a/src/masonite/middleware/GuardMiddleware.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Guard Middleware.""" - -from ..auth import Auth -from ..helpers import config - - -class GuardMiddleware: - """Middleware to switch the guard""" - - def __init__(self, auth: Auth): - self.auth = auth - - def before(self, guard): - """Sets specified guard for the request. - - Arguments: - guard {string} -- The key of the guard to set. - """ - self.auth.set(guard) - - def after(self, _): - """Sets the default guard back after the request. - - Arguments: - _ {ignored} -- ignored - """ - self.auth.set(config("auth.auth.defaults.guard")) diff --git a/src/masonite/middleware/MaintenanceModeMiddleware.py b/src/masonite/middleware/MaintenanceModeMiddleware.py deleted file mode 100644 index d2e7cc1a9..000000000 --- a/src/masonite/middleware/MaintenanceModeMiddleware.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Maintainance Mode Middleware.""" -import os -from ..response import Response -from config import application - - -class MaintenanceModeMiddleware: - def __init__(self, response: Response): - self.response = response - - def before(self): - down = os.path.exists( - os.path.join(application.BASE_DIRECTORY, "bootstrap/down") - ) - if down is True: - self.response.status(503) diff --git a/src/masonite/middleware/ResponseMiddleware.py b/src/masonite/middleware/ResponseMiddleware.py deleted file mode 100644 index 8a23faefa..000000000 --- a/src/masonite/middleware/ResponseMiddleware.py +++ /dev/null @@ -1,21 +0,0 @@ -from ..app import App -from ..request import Request -from ..response import Response - - -class ResponseMiddleware: - def __init__(self, request: Request, app: App, response: Response): - self.request = request - self.app = app - self.response = response - - def after(self): - if self.request.redirect_url: - self.response.redirect(self.request.redirect_url, status=302) - self.request.reset_redirections() - - if self.app.has("Session") and self.response.is_status(200): - try: - self.app.make("Session").driver("memory").reset(flash_only=True) - except Exception: - pass diff --git a/src/masonite/middleware/SecureHeadersMiddleware.py b/src/masonite/middleware/SecureHeadersMiddleware.py deleted file mode 100644 index fb5125085..000000000 --- a/src/masonite/middleware/SecureHeadersMiddleware.py +++ /dev/null @@ -1,40 +0,0 @@ -"""SecureHeaders Middleware.""" - -from ..response import Response - - -class SecureHeadersMiddleware: - """SecureHeaders Middleware.""" - - def __init__(self, response: Response): - """Inject Any Dependencies From The Service Container. - - Arguments: - Response {masonite.response.Response} -- The Masonite response object - """ - self.response = response - self.headers = { - "Strict-Transport-Security": "max-age=63072000; includeSubdomains", - "X-Frame-Options": "SAMEORIGIN", - "X-XSS-Protection": "1; mode=block", - "X-Content-Type-Options": "nosniff", - "Referrer-Policy": "no-referrer, strict-origin-when-cross-origin", - "Cache-control": "no-cache, no-store, must-revalidate", - "Pragma": "no-cache", - } - - def before(self): - """Run This Middleware Before The Route Executes.""" - pass - - def after(self): - """Run This Middleware After The Route Executes.""" - from config import middleware - - try: - # Try importing secure headers if they exist in the config file - self.headers.update(middleware.SECURE_HEADERS) - except AttributeError: - pass - - self.response.header(self.headers) diff --git a/src/masonite/middleware/__init__.py b/src/masonite/middleware/__init__.py index 87d11f2c0..16f53ce9d 100644 --- a/src/masonite/middleware/__init__.py +++ b/src/masonite/middleware/__init__.py @@ -1,6 +1,6 @@ -from .CsrfMiddleware import CsrfMiddleware -from .GuardMiddleware import GuardMiddleware -from .CorsMiddleware import CorsMiddleware -from .MaintenanceModeMiddleware import MaintenanceModeMiddleware -from .ResponseMiddleware import ResponseMiddleware -from .SecureHeadersMiddleware import SecureHeadersMiddleware +from .middleware_capsule import MiddlewareCapsule +from .middleware import Middleware +from .route.VerifyCsrfToken import VerifyCsrfToken +from .route.SessionMiddleware import SessionMiddleware +from .route.EncryptCookies import EncryptCookies +from .route.LoadUserMiddleware import LoadUserMiddleware diff --git a/src/masonite/middleware/middleware.py b/src/masonite/middleware/middleware.py new file mode 100644 index 000000000..8fffa40aa --- /dev/null +++ b/src/masonite/middleware/middleware.py @@ -0,0 +1,2 @@ +class Middleware: + pass diff --git a/src/masonite/middleware/middleware_capsule.py b/src/masonite/middleware/middleware_capsule.py new file mode 100644 index 000000000..5722be99a --- /dev/null +++ b/src/masonite/middleware/middleware_capsule.py @@ -0,0 +1,64 @@ +class MiddlewareCapsule: + def __init__(self): + self.route_middleware = {} + self.http_middleware = [] + + def add(self, middleware): + if isinstance(middleware, dict): + self.route_middleware.update(middleware) + + if isinstance(middleware, list): + self.http_middleware += middleware + + return self + + def remove(self, middleware): + if middleware in self.route_middleware: + self.route_middleware.pop(middleware) + elif middleware in self.http_middleware: + self.http_middleware.pop(self.http_middleware.index(middleware)) + return self + + def get_route_middleware(self, keys=None): + middlewares = [] + if keys is None: + return self.route_middleware + + if keys is None: + keys = [] + + for key in keys: + found = self.route_middleware[key] + if isinstance(found, list): + middlewares += found + else: + middlewares += [found] + return middlewares + + def get_http_middleware(self): + return self.http_middleware + + def run_route_middleware(self, middlewares, request, response, callback="before"): + for middleware in middlewares: + if ":" in middleware: + # get list of arguments if any + middleware_to_run, raw_arguments = middleware.split(":") + raw_arguments = raw_arguments.split(",") + # try to parse arguments with @ from requests + arguments = [] + for arg in raw_arguments: + if "@" in arg: + arg = arg.replace("@", "") + arg = request.input(arg) + arguments.append(arg) + arguments = tuple(arguments) + else: + middleware_to_run = middleware + arguments = () + route_middlewares = self.get_route_middleware([middleware_to_run]) + for route_middleware in route_middlewares: + middleware_response = getattr(route_middleware(), callback)( + request, response, *arguments + ) + if middleware_response != request: + break diff --git a/src/masonite/middleware/route/EncryptCookies.py b/src/masonite/middleware/route/EncryptCookies.py new file mode 100644 index 000000000..6995d31e7 --- /dev/null +++ b/src/masonite/middleware/route/EncryptCookies.py @@ -0,0 +1,21 @@ +from ...exceptions import InvalidToken + + +class EncryptCookies: + def before(self, request, response): + for _, cookie in request.cookie_jar.all().items(): + try: + cookie.value = request.app.make("sign").unsign(cookie.value) + except InvalidToken: + pass + + return request + + def after(self, request, response): + for _, cookie in response.cookie_jar.all().items(): + try: + cookie.value = request.app.make("sign").sign(cookie.value) + except InvalidToken: + pass + + return request diff --git a/src/masonite/middleware/route/LoadUserMiddleware.py b/src/masonite/middleware/route/LoadUserMiddleware.py new file mode 100644 index 000000000..00669f21e --- /dev/null +++ b/src/masonite/middleware/route/LoadUserMiddleware.py @@ -0,0 +1,11 @@ +from .. import Middleware +from ...facades import Auth + + +class LoadUserMiddleware(Middleware): + def before(self, request, _): + request.set_user(Auth.user()) + return request + + def after(self, request, _): + return request diff --git a/src/masonite/middleware/route/SessionMiddleware.py b/src/masonite/middleware/route/SessionMiddleware.py new file mode 100644 index 000000000..cc91b15b1 --- /dev/null +++ b/src/masonite/middleware/route/SessionMiddleware.py @@ -0,0 +1,29 @@ +from .. import Middleware +from ...utils.str import random_string +from ...facades import Request, Session, Response + + +class SessionMiddleware(Middleware): + def before(self, request, response): + if not request.cookie("SESSID"): + session_code = random_string(10) + response.cookie("SESSID", session_code) + request.cookie("SESSID", session_code) + Session.start() + request.app.make("response").with_input = self.with_input + request.app.make("response").with_errors = self.with_errors + request.app.make("request").session = Session + return request + + def after(self, request, _): + Session.save() + return request + + def with_input(self): + for key, value in Request.all().items(): + Session.flash(key, value) + return Response + + def with_errors(self, errors): + Session.flash("errors", errors) + return Response diff --git a/src/masonite/middleware/route/VerifyCsrfToken.py b/src/masonite/middleware/route/VerifyCsrfToken.py new file mode 100644 index 000000000..3ac55c02c --- /dev/null +++ b/src/masonite/middleware/route/VerifyCsrfToken.py @@ -0,0 +1,69 @@ +from .. import Middleware +from markupsafe import Markup +from ...exceptions import InvalidCSRFToken +from hmac import compare_digest + + +class VerifyCsrfToken(Middleware): + + exempt = [] + + def before(self, request, response): + self.verify_token(request, self.get_token(request)) + + token = self.create_token(request, response) + + request.app.make("view").share( + { + "csrf_field": Markup( + f"" + ), + "csrf_token": token, + } + ) + + return request + + def after(self, request, response): + return request + + def create_token(self, request, response): + session = request.cookie("SESSID") + response.cookie("csrf_token", session) + return session + + def verify_token(self, request, token): + if self.in_exempt(request): + return True + if request.is_not_safe() and not token: + raise InvalidCSRFToken("Missing CSRF Token") + if request.is_not_safe(): + if request.cookie("csrf_token") and ( + compare_digest( + request.cookie("csrf_token"), + token, + ) + and compare_digest(token, request.cookie("SESSID")) + ): + return True + raise InvalidCSRFToken("Invalid CSRF token.") + return True + + def in_exempt(self, request): + """Determine if the request has a URI that should pass through CSRF verification. + + Returns: + bool + """ + for route in self.exempt: + if request.contains(route): + return True + + return False + + def get_token(self, request): + return ( + request.header("X-CSRF-TOKEN") + or request.header("X-XSRF-TOKEN") + or request.input("__token") + ) diff --git a/config/__init__.py b/src/masonite/middleware/route/__init__.py similarity index 100% rename from config/__init__.py rename to src/masonite/middleware/route/__init__.py diff --git a/src/masonite/notification/AnonymousNotifiable.py b/src/masonite/notification/AnonymousNotifiable.py new file mode 100644 index 000000000..d54bf5bcf --- /dev/null +++ b/src/masonite/notification/AnonymousNotifiable.py @@ -0,0 +1,39 @@ +"""Anonymous Notifiable mixin""" + +from .Notifiable import Notifiable + + +class AnonymousNotifiable(Notifiable): + """Anonymous notifiable allowing to send notification without having + a notifiable entity. + + Usage: + self.notification.route("sms", "+3346474764").send(WelcomeNotification()) + """ + + def __init__(self, application=None): + self.application = application + self._routes = {} + + def route(self, driver, recipient): + """Define which driver using to route the notification.""" + if driver == "database": + raise ValueError( + "The database driver does not support on-demand notifications." + ) + self._routes[driver] = recipient + return self + + def route_notification_for(self, driver): + try: + return self._routes[driver] + except KeyError: + raise ValueError( + "Routing has not been defined for the driver {}".format(driver) + ) + + def send(self, notification, dry=False, fail_silently=False): + """Send the given notification.""" + return self.application.make("notification").send( + self, notification, self._routes, dry, fail_silently + ) diff --git a/src/masonite/notification/DatabaseNotification.py b/src/masonite/notification/DatabaseNotification.py new file mode 100644 index 000000000..e59795494 --- /dev/null +++ b/src/masonite/notification/DatabaseNotification.py @@ -0,0 +1,38 @@ +"""DatabaseNotification Model.""" +import pendulum +from masoniteorm.relationships import morph_to +from masoniteorm.models import Model + + +class DatabaseNotification(Model): + """DatabaseNotification Model allowing notifications to be stored in database.""" + + __fillable__ = ["id", "type", "data", "read_at", "notifiable_id", "notifiable_type"] + __table__ = "notifications" + + @morph_to("notifiable_type", "notifiable_id") + def notifiable(self): + """Get the notifiable entity that the notification belongs to.""" + return + + def mark_as_read(self): + """Mark the notification as read.""" + if not self.read_at: + self.read_at = pendulum.now() + return self.save(query=True) + + def mark_as_unread(self): + """Mark the notification as unread.""" + if self.read_at: + self.read_at = None + return self.save(query=True) + + @property + def is_read(self): + """Determine if a notification has been read.""" + return self.read_at is not None + + @property + def is_unread(self): + """Determine if a notification has not been read yet.""" + return self.read_at is None diff --git a/src/masonite/notification/MockNotification.py b/src/masonite/notification/MockNotification.py new file mode 100644 index 000000000..f9c40e3c2 --- /dev/null +++ b/src/masonite/notification/MockNotification.py @@ -0,0 +1,120 @@ +from .NotificationManager import NotificationManager +from .AnonymousNotifiable import AnonymousNotifiable +from .Notification import Notification + + +class NotificationWithAsserts(Notification): + def assertSentVia(self, *drivers): + sent_via = self.via(self.notifiable) + for driver in drivers: + assert ( + driver in sent_via + ), f"notification sent via {sent_via}, not {driver}." + return self + + def assertEqual(self, value, reference): + assert value == reference, "{value} not equal to {reference}." + return self + + def assertNotEqual(self, value, reference): + assert value != reference, "{value} equal to {reference}." + return self + + def assertIn(self, value, container): + assert value in container, "{value} not in {container}." + return self + + @classmethod + def patch(cls, target): + for k in cls.__dict__: + obj = getattr(cls, k) + if not k.startswith("_") and callable(obj): + setattr(target, k, obj) + + +class MockNotification(NotificationManager): + def __init__(self, application, *args, **kwargs): + super().__init__(application, *args, **kwargs) + self.count = 0 + self.last_notifiable = None + self.last_notification = None + + def send( + self, notifiables, notification, drivers=[], dry=False, fail_silently=False + ): + _notifiables = [] + for notifiable in self._format_notifiables(notifiables): + if isinstance(notifiable, AnonymousNotifiable): + _notifiables.extend(notifiable._routes.values()) + else: + _notifiables.append(notifiable) + + notification_key = notification.type() + NotificationWithAsserts.patch(notification.__class__) + for notifiable in _notifiables: + notification.notifiable = notifiable # for asserts + old_notifs = self.sent_notifications.get(notifiable, {}) + old_notifs.update( + { + notification_key: old_notifs.get(notification_key, []) + + [notification] + } + ) + self.sent_notifications.update({notifiable: old_notifs}) + self.count += 1 + self.last_notification = notification + self.last_notifiable = notifiable + return self + + def resetCount(self): + """Reset sent notifications count.""" + self.count = 0 + self.sent_notifications = {} + self.last_notifiable = None + self.last_notification = None + return self + + def assertNothingSent(self): + assert self.count == 0, f"{self.count} notifications have been sent." + return self + + def assertCount(self, count): + assert ( + self.count == count + ), f"{self.count} notifications have been sent, not {count}." + return self + + def assertSentTo( + self, notifiable, notification_class, callable_assert=None, count=None + ): + notification_key = notification_class.__name__ + notifiable_notifications = self.sent_notifications.get(notifiable, []) + assert notification_key in notifiable_notifications + if count: + sent_count = len(notifiable_notifications.get(notification_key, [])) + assert ( + sent_count == count + ), f"{notification_key} has been sent to {notifiable} {sent_count} times" + if callable_assert: + # assert last notification sent for this notifiable + notification = notifiable_notifications.get(notification_key)[-1] + assert callable_assert(notifiable, notification) + return self + + def last(self): + """Get last sent mocked notification if any.""" + return self.last_notification + + def assertLast(self, callable_assert): + if not self.last_notifiable or not self.last_notification: + raise AssertionError("No notification has been sent.") + assert callable_assert(self.last_notifiable, self.last_notification) + return self + + def assertNotSentTo(self, notifiable, notification_class): + notification_key = notification_class.__name__ + notifiable_notifications = self.sent_notifications.get(notifiable, []) + assert ( + notification_key not in notifiable_notifications + ), f"{notification_key} has been sent to {notifiable}." + return self diff --git a/src/masonite/notification/Notifiable.py b/src/masonite/notification/Notifiable.py new file mode 100644 index 000000000..c1c467220 --- /dev/null +++ b/src/masonite/notification/Notifiable.py @@ -0,0 +1,63 @@ +"""Notifiable mixin""" +from masoniteorm.relationships import has_many + +from .DatabaseNotification import DatabaseNotification +from ..exceptions.exceptions import NotificationException + + +class Notifiable: + """Notifiable mixin allowing to send notification to a model. It's often used with the + User model. + + Usage: + user.notify(WelcomeNotification()) + """ + + def notify(self, notification, drivers=[], dry=False, fail_silently=False): + """Send the given notification.""" + from wsgi import application + + return application.make("notification").send( + self, notification, drivers, dry, fail_silently + ) + + def route_notification_for(self, driver): + """Get the notification routing information for the given driver. If method has not been + defined on the model: for mail driver try to use 'email' field of model.""" + # check if routing has been specified on the model + method_name = "route_notification_for_{0}".format(driver) + + try: + method = getattr(self, method_name) + return method() + except AttributeError: + # if no method is defined on notifiable use default + if driver == "database": + # with database channel, notifications are saved to database + pass + elif driver == "mail": + return self.email + else: + raise NotificationException( + "Notifiable model does not implement {}".format(method_name) + ) + + @has_many("id", "notifiable_id") + def notifications(self): + """Get all notifications sent to the model instance. Only for 'database' + notifications.""" + return DatabaseNotification.where("notifiable_type", "users").order_by( + "created_at", direction="DESC" + ) + + @property + def unread_notifications(self): + """Get the model instance unread notifications. Only for 'database' + notifications.""" + return self.notifications.where("read_at", "==", None) + + @property + def read_notifications(self): + """Get the model instance read notifications. Only for 'database' + notifications.""" + return self.notifications.where("read_at", "!=", None) diff --git a/src/masonite/notification/Notification.py b/src/masonite/notification/Notification.py new file mode 100644 index 000000000..9655c682d --- /dev/null +++ b/src/masonite/notification/Notification.py @@ -0,0 +1,39 @@ +"""Base Notification facade.""" + + +class Notification: + def via(self, notifiable): + """Defines the notification's delivery channels.""" + raise NotImplementedError("via() method should be implemented.") + + def should_send(self): + return True + + def ignore_errors(self): + return False + + def broadcast_on(self): + return "broadcast" + + @classmethod + def type(cls): + """Get notification type defined with class name.""" + return cls.__name__ + + def dry(self): + """Sets whether the notification should be sent or not. + + Returns: + self + """ + self._dry = True + return self + + def fail_silently(self): + """Sets whether the notification can fail silently (without raising exceptions). + + Returns: + self + """ + self._fail_silently = True + return self diff --git a/src/masonite/notification/NotificationManager.py b/src/masonite/notification/NotificationManager.py new file mode 100644 index 000000000..2f5818532 --- /dev/null +++ b/src/masonite/notification/NotificationManager.py @@ -0,0 +1,85 @@ +"""Notification handler class""" +import uuid + +from ..exceptions.exceptions import NotificationException +from ..queues import ShouldQueue +from .AnonymousNotifiable import AnonymousNotifiable + + +class NotificationManager: + """Notification handler which handle sending/queuing notifications anonymously + or to notifiables through different channels.""" + + sent_notifications = {} + dry_notifications = {} + + def __init__(self, application, driver_config=None): + self.application = application + self.drivers = {} + self.driver_config = driver_config or {} + self.options = {"dry": False} + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + self.get_driver(name).set_options(self.get_config_options(name)) + + def get_driver(self, name): + return self.drivers[name] + + def set_configuration(self, config): + self.driver_config = config.get("drivers") + self.options.update({"dry": config.get("dry")}) + return self + + def get_config_options(self, driver): + return self.driver_config.get(driver, {}) + + def send( + self, notifiables, notification, drivers=[], dry=False, fail_silently=False + ): + """Send the given notification to the given notifiables.""" + notifiables = self._format_notifiables(notifiables) + if not notification.should_send() or dry or self.options.get("dry"): + key = notification.type() + self.dry_notifications.update( + {key: notifiables + self.dry_notifications.get(key, [])} + ) + return + results = [] + for notifiable in notifiables: + # get drivers to use for sending this notification + drivers = drivers if drivers else notification.via(notifiable) + if not drivers: + raise NotificationException( + "No drivers have been defined in via() method of {0} notification.".format( + notification.type() + ) + ) + notification.id = uuid.uuid4() + for driver in drivers: + driver_instance = self.get_driver(driver) + if isinstance(notifiable, AnonymousNotifiable) and driver == "database": + # this case is not possible but that should not stop other channels to be used + continue + try: + # if isinstance(notification, ShouldQueue): + # results.append(driver_instance.queue(notifiable, notification)) + # else: + results.append(driver_instance.send(notifiable, notification)) + except Exception as e: + if not notification.ignore_errors() and not fail_silently: + raise e + + return results[0] if len(results) == 1 else results + + def _format_notifiables(self, notifiables): + from masoniteorm.collection import Collection + + if isinstance(notifiables, (list, tuple, Collection)): + return notifiables + else: + return [notifiables] + + def route(self, driver, route): + """Specify how to send a notification to an anonymous notifiable.""" + return AnonymousNotifiable(self.application).route(driver, route) diff --git a/src/masonite/notification/SlackMessage.py b/src/masonite/notification/SlackMessage.py new file mode 100644 index 000000000..181317873 --- /dev/null +++ b/src/masonite/notification/SlackMessage.py @@ -0,0 +1,166 @@ +"""Class modelling a Slack message.""" +import json + + +class SlackMessage: + WEBHOOK_MODE = 1 + API_MODE = 2 + + def __init__(self): + self._text = "" + self._username = "masonite-bot" + self._icon_emoji = "" + self._icon_url = "" + self._text = "" + self._mrkdwn = True + self._as_current_user = False + self._reply_broadcast = False + # Indicates if channel names and usernames should be linked. + self._link_names = False + # Indicates if you want a preview of links inlined in the message. + self._unfurl_links = False + # Indicates if you want a preview of links to media inlined in the message. + self._unfurl_media = False + self._blocks = [] + + self._token = "" + self._webhook = "" + self._mode = None + + def from_(self, username, icon=None, url=None): + """Set a custom username and optional emoji icon for the Slack message.""" + self._username = username + if icon: + self._icon_emoji = icon + elif url: + self._icon_url = url + return self + + def to(self, to): + """Specifies the channel to send the message to. It can be a list or single + element. It can be either a channel ID or a channel name (with #), if it's + a channel name the channel ID will be resolved later. + """ + self._to = to + return self + + def text(self, text): + """Specifies the text to be sent in the message. + + Arguments: + text {string} -- The text to show in the message. + + Returns: + self + """ + self._text = text + return self + + def link_names(self): + """Find and link channel names and usernames in message.""" + self._link_names = True + return self + + def unfurl_links(self): + """Whether the message should unfurl any links. + + Unfurling is when it shows a bigger part of the message after the text is sent + like when pasting a link and it showing the header images. + + Returns: + self + """ + self._unfurl_links = True + self._unfurl_media = True + return self + + def without_markdown(self): + """Specifies whether the message should explicitly not honor markdown text. + + Returns: + self + """ + self._mrkdwn = False + return self + + def can_reply(self): + """Whether the message should be ably to be replied back to. + + Returns: + self + """ + self._reply_broadcast = True + return self + + def build(self, *args, **kwargs): + return self + + def get_options(self): + options = { + "text": self._text, + # optional + "link_names": self._link_names, + "unfurl_links": self._unfurl_links, + "unfurl_media": self._unfurl_media, + "username": self._username, + "as_user": self._as_current_user, + "icon_emoji": self._icon_emoji, + "icon_url": self._icon_url, + "mrkdwn": self._mrkdwn, + "reply_broadcast": self._reply_broadcast, + "blocks": json.dumps([block._resolve() for block in self._blocks]), + } + if self._mode == self.API_MODE: + options.update({"channel": self._to, "token": self._token}) + return options + + def token(self, token): + """[API_MODE only] Specifies the token to use for Slack authentication. + + Arguments: + token {string} -- The Slack authentication token. + + Returns: + self + """ + self._token = token + return self + + def as_current_user(self): + """[API_MODE only] Send message as the currently authenticated user. + + Returns: + self + """ + self._as_current_user = True + return self + + def webhook(self, webhook): + """[WEBHOOK_MODE only] Specifies the webhook to use to send the message and authenticate + to Slack. + + Arguments: + webhook {string} -- Slack configured webhook url. + + Returns: + self + """ + self._webhook = webhook + return self + + def block(self, block_instance): + try: + from slackblocks.blocks import Block + except ImportError: + raise ModuleNotFoundError( + "Could not find the 'slackblocks' library. Run 'pip install slackblocks' to fix this." + ) + + if not isinstance(block_instance, Block): + raise Exception("Blocks should be imported from 'slackblocks' package.") + self._blocks.append(block_instance) + return self + + def mode(self, mode): + self._mode = mode + return self diff --git a/src/masonite/notification/Sms.py b/src/masonite/notification/Sms.py new file mode 100644 index 000000000..271098b77 --- /dev/null +++ b/src/masonite/notification/Sms.py @@ -0,0 +1,54 @@ +"""Sms Component""" + + +class Sms: + + _from = "" + _to = "" + _text = "" + _client_ref = "" + _type = "text" + + def __init__(self, text=""): + self._text = text + + def from_(self, number): + """Set the name or number the message should be sent from. Numbers should + be specified in E.164 format. Details can be found here: + https://developer.nexmo.com/messaging/sms/guides/custom-sender-id""" + self._from = number + return self + + def text(self, text): + self._text = text + return self + + def to(self, to): + self._to = to + return self + + def set_unicode(self): + """Set message as unicode to handle unicode characters in text.""" + self._type = "unicode" + return self + + def client_ref(self, client_ref): + """Set your own client reference (up to 40 characters).""" + if len(client_ref) > 40: + raise ValueError("client_ref should have less then 40 characters.") + self._client_ref = client_ref + return self + + def build(self, *args, **kwargs): + return self + + def get_options(self): + base_dict = { + "to": self._to, + "from": self._from, + "text": self._text, + "type": self._type, + } + if self._client_ref: + base_dict.update({"client-ref": self._client_ref}) + return base_dict diff --git a/src/masonite/notification/Textable.py b/src/masonite/notification/Textable.py new file mode 100644 index 000000000..a1fa53726 --- /dev/null +++ b/src/masonite/notification/Textable.py @@ -0,0 +1,6 @@ +from .Sms import Sms + + +class Textable: + def text_message(self, message): + return Sms().text(message) diff --git a/src/masonite/notification/__init__.py b/src/masonite/notification/__init__.py new file mode 100644 index 000000000..0db22d80c --- /dev/null +++ b/src/masonite/notification/__init__.py @@ -0,0 +1,9 @@ +from .NotificationManager import NotificationManager +from .MockNotification import MockNotification +from .DatabaseNotification import DatabaseNotification +from .Notification import Notification +from .Notifiable import Notifiable +from .AnonymousNotifiable import AnonymousNotifiable +from .Sms import Sms +from .SlackMessage import SlackMessage +from .Textable import Textable diff --git a/src/masonite/notification/commands/MakeNotificationCommand.py b/src/masonite/notification/commands/MakeNotificationCommand.py new file mode 100644 index 000000000..0cb20ec25 --- /dev/null +++ b/src/masonite/notification/commands/MakeNotificationCommand.py @@ -0,0 +1,42 @@ +"""New Notification Command""" +from cleo import Command +import inflection +import os + +from ...utils.filesystem import get_module_dir, make_directory, render_stub_file +from ...utils.location import base_path +from ...utils.str import as_filepath + + +class MakeNotificationCommand(Command): + """ + Creates a new notification class. + + notification + {name : Name of the notification} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + + content = render_stub_file(self.get_stub_notification_path(), name) + + relative_filename = os.path.join( + as_filepath(self.app.make("notifications.location")), name + ".py" + ) + filepath = base_path(relative_filename) + make_directory(filepath) + + with open(filepath, "w") as f: + f.write(content) + + self.info(f"Notification Created ({relative_filename})") + + def get_stub_notification_path(self): + return os.path.join( + get_module_dir(__file__), "../../stubs/notification/Notification.py" + ) diff --git a/src/masonite/notification/commands/NotificationTableCommand.py b/src/masonite/notification/commands/NotificationTableCommand.py new file mode 100644 index 000000000..ee9a24763 --- /dev/null +++ b/src/masonite/notification/commands/NotificationTableCommand.py @@ -0,0 +1,37 @@ +"""Notification Table Command.""" +from cleo import Command +import os + +from ...utils.filesystem import get_module_dir, make_directory +from ...utils.time import migration_timestamp +from ...utils.location import base_path + + +class NotificationTableCommand(Command): + """ + Creates the notifications table needed for storing notifications in the database. + + notification:table + {--d|--directory=database/migrations : Specifies the directory to create the migration in} + """ + + def handle(self): + with open( + os.path.join( + get_module_dir(__file__), + "../../stubs/notification/create_notifications_table.py", + ) + ) as fp: + output = fp.read() + + relative_filename = os.path.join( + self.option("directory"), + f"{migration_timestamp()}_create_notifications_table.py", + ) + filepath = base_path(relative_filename) + make_directory(filepath) + + with open(filepath, "w") as fp: + fp.write(output) + + self.info(f"Migration file created: {relative_filename}") diff --git a/src/masonite/notification/commands/__init__.py b/src/masonite/notification/commands/__init__.py new file mode 100644 index 000000000..7ea3fdcbc --- /dev/null +++ b/src/masonite/notification/commands/__init__.py @@ -0,0 +1,2 @@ +from .MakeNotificationCommand import MakeNotificationCommand +from .NotificationTableCommand import NotificationTableCommand diff --git a/src/masonite/notification/drivers/BaseDriver.py b/src/masonite/notification/drivers/BaseDriver.py new file mode 100644 index 000000000..04b412488 --- /dev/null +++ b/src/masonite/notification/drivers/BaseDriver.py @@ -0,0 +1,19 @@ +class BaseDriver: + def send(self, notifiable, notification): + """Implements sending the notification to notifiables through + this driver.""" + raise NotImplementedError( + "send() method must be implemented for a notification driver." + ) + + def get_data(self, driver, notifiable, notification): + """Get the data for the notification.""" + method_name = f"to_{driver}" + try: + method = getattr(notification, method_name) + except AttributeError: + raise NotImplementedError( + f"Notification model should implement {method_name}() method." + ) + else: + return method(notifiable) diff --git a/src/masonite/notification/drivers/BroadcastDriver.py b/src/masonite/notification/drivers/BroadcastDriver.py new file mode 100644 index 000000000..cdda012c0 --- /dev/null +++ b/src/masonite/notification/drivers/BroadcastDriver.py @@ -0,0 +1,22 @@ +"""Broadcast notification driver.""" + +from .BaseDriver import BaseDriver + + +class BroadcastDriver(BaseDriver): + def __init__(self, application): + self.application = application + self.options = {} + + def set_options(self, options): + self.options = options + return self + + def send(self, notifiable, notification): + """Used to broadcast a notification.""" + data = self.get_data("broadcast", notifiable, notification) + channels = notification.broadcast_on() or notifiable.route_notification_for( + "broadcast" + ) + event = notification.type() + self.application.make("broadcast").channel(channels, event, data) diff --git a/src/masonite/notification/drivers/DatabaseDriver.py b/src/masonite/notification/drivers/DatabaseDriver.py new file mode 100644 index 000000000..146c58d55 --- /dev/null +++ b/src/masonite/notification/drivers/DatabaseDriver.py @@ -0,0 +1,37 @@ +"""Database notification driver.""" +import json + +from .BaseDriver import BaseDriver + + +class DatabaseDriver(BaseDriver): + def __init__(self, application): + self.application = application + self.options = {} + + def set_options(self, options): + self.options = options + return self + + def get_builder(self): + return ( + self.application.make("builder") + .on(self.options.get("connection")) + .table(self.options.get("table")) + ) + + def send(self, notifiable, notification): + """Used to send the email and run the logic for sending emails.""" + data = self.build(notifiable, notification) + return self.get_builder().new().create(data) + + def build(self, notifiable, notification): + """Build an array payload for the DatabaseNotification Model.""" + return { + "id": str(notification.id), + "type": notification.type(), + "notifiable_id": notifiable.id, + "notifiable_type": notifiable.get_table_name(), + "data": json.dumps(self.get_data("database", notifiable, notification)), + "read_at": None, + } diff --git a/src/masonite/notification/drivers/MailDriver.py b/src/masonite/notification/drivers/MailDriver.py new file mode 100644 index 000000000..a620f6cfb --- /dev/null +++ b/src/masonite/notification/drivers/MailDriver.py @@ -0,0 +1,22 @@ +"""Mail notification driver.""" + +from .BaseDriver import BaseDriver + + +class MailDriver(BaseDriver): + def __init__(self, application): + self.application = application + self.options = {} + + def set_options(self, options): + self.options = options + return self + + def send(self, notifiable, notification): + """Used to send the email.""" + mailable = self.get_data("mail", notifiable, notification) + if not mailable._to: + recipients = notifiable.route_notification_for("mail") + mailable = mailable.to(recipients) + # TODO: allow changing driver how ????? + return self.application.make("mail").mailable(mailable).send(driver="terminal") diff --git a/src/masonite/notification/drivers/SlackDriver.py b/src/masonite/notification/drivers/SlackDriver.py new file mode 100644 index 000000000..2d297419f --- /dev/null +++ b/src/masonite/notification/drivers/SlackDriver.py @@ -0,0 +1,110 @@ +"""Slack notification driver""" +import requests + +from ...exceptions import NotificationException +from .BaseDriver import BaseDriver + + +class SlackDriver(BaseDriver): + + WEBHOOK_MODE = 1 + API_MODE = 2 + send_url = "https://slack.com/api/chat.postMessage" + channel_url = "https://slack.com/api/conversations.list" + + def __init__(self, application): + self.application = application + self.options = {} + self.mode = self.WEBHOOK_MODE + + def set_options(self, options): + self.options = options + return self + + def send(self, notifiable, notification): + """Used to send the notification to slack.""" + slack_message = self.build(notifiable, notification) + if slack_message._mode == self.WEBHOOK_MODE: + self.send_via_webhook(slack_message) + else: + self.send_via_api(slack_message) + + def build(self, notifiable, notification): + """Build Slack message payload sent to Slack API or through Slack webhook.""" + slack_message = self.get_data("slack", notifiable, notification) + recipients = self.get_recipients(notifiable) + mode = self.get_sending_mode(recipients) + slack_message = slack_message.mode(mode) + + if mode == self.WEBHOOK_MODE: + slack_message = slack_message.to(recipients) + elif mode == self.API_MODE: + slack_message = slack_message.to(recipients) + if not slack_message._token: + slack_message = slack_message.token(self.options.get("token")) + return slack_message + + def get_recipients(self, notifiable): + recipients = notifiable.route_notification_for("slack") + if not isinstance(recipients, (list, tuple)): + recipients = [recipients] + return recipients + + def get_sending_mode(self, recipients): + modes = [] + for recipient in recipients: + if recipient.startswith("https://hooks.slack.com"): + modes.append(self.WEBHOOK_MODE) + else: + modes.append(self.API_MODE) + if len(set(modes)) > 1: + raise NotificationException("Slack sending mode cannot be mixed.") + return modes[0] + + def send_via_webhook(self, slack_message): + webhook_urls = slack_message._to + payload = slack_message.build().get_options() + for webhook_url in webhook_urls: + response = requests.post( + webhook_url, + payload, + headers={"Content-Type": "application/json"}, + ) + if response.status_code != 200: + raise NotificationException( + "{}. Check Slack webhooks docs.".format(response.text) + ) + + def send_via_api(self, slack_message): + """Send Slack notification with Slack Web API as documented + here https://api.slack.com/methods/chat.postMessage""" + channels = slack_message._to + for channel in channels: + channel = self.convert_channel(channel, slack_message._token) + # set only one recipient at a time + slack_message.to(channel) + payload = slack_message.build().get_options() + response = requests.post(self.send_url, payload).json() + if not response["ok"]: + raise NotificationException( + "{}. Check Slack API docs.".format(response["error"]) + ) + else: + return response + + def convert_channel(self, name, token): + """Calls the Slack API to find the channel ID if not already a channel ID. + + Arguments: + name {string} -- The channel name to find. + """ + if "#" not in name: + return name + response = requests.post(self.channel_url, {"token": token}).json() + for channel in response["channels"]: + if channel["name"] == name.split("#")[1]: + return channel["id"] + + raise NotificationException( + f"The user or channel being addressed either do not exist or is invalid: {name}" + ) diff --git a/src/masonite/notification/drivers/__init__.py b/src/masonite/notification/drivers/__init__.py new file mode 100644 index 000000000..04c637c26 --- /dev/null +++ b/src/masonite/notification/drivers/__init__.py @@ -0,0 +1,5 @@ +from .BroadcastDriver import BroadcastDriver +from .DatabaseDriver import DatabaseDriver +from .MailDriver import MailDriver +from .SlackDriver import SlackDriver +from .vonage.VonageDriver import VonageDriver diff --git a/src/masonite/notification/drivers/vonage/VonageDriver.py b/src/masonite/notification/drivers/vonage/VonageDriver.py new file mode 100644 index 000000000..f2ac770fa --- /dev/null +++ b/src/masonite/notification/drivers/vonage/VonageDriver.py @@ -0,0 +1,69 @@ +"""Vonage notification driver.""" +from ....exceptions import NotificationException +from ..BaseDriver import BaseDriver + + +class VonageDriver(BaseDriver): + def __init__(self, application): + self.app = application + self.options = {} + + def set_options(self, options): + self.options = options + return self + + def build(self, notifiable, notification): + """Build SMS payload sent to Vonage API.""" + sms = self.get_data("vonage", notifiable, notification) + if not sms._from: + sms = sms.from_(self.options.get("sms_from")) + if not sms._to: + recipients = notifiable.route_notification_for("vonage") + sms = sms.to(recipients) + return sms + + def get_sms_client(self): + try: + import vonage + from vonage.sms import Sms + except ImportError: + raise ModuleNotFoundError( + "Could not find the 'vonage' library. Run 'pip install vonage' to fix this." + ) + client = vonage.Client( + key=self.options.get("key"), secret=self.options.get("secret") + ) + return Sms(client) + + def send(self, notifiable, notification): + """Used to send the SMS.""" + sms = self.build(notifiable, notification) + client = self.get_sms_client() + recipients = sms._to + for recipient in recipients: + payload = sms.to(recipient).build().get_options() + response = client.send_message(payload) + self._handle_errors(response) + return response + + def _handle_errors(self, response): + """Handle errors of Vonage API. Raises VonageAPIError if request does + not succeed. + + An error message is structured as follows: + {'message-count': '1', 'messages': [{'status': '2', 'error-text': 'Missing api_key'}]} + As a success message can be structured as follows: + {'message-count': '1', 'messages': [{'to': '3365231278', 'message-id': '140000012BD37332', 'status': '0', + 'remaining-balance': '1.87440000', 'message-price': '0.06280000', 'network': '20810'}]} + + More informations on status code errors: https://developer.nexmo.com/api-errors/sms + + """ + for message in response.get("messages", []): + status = message["status"] + if status != "0": + raise NotificationException( + "Vonage Code [{0}]: {1}. Please refer to API documentation for more details.".format( + status, message["error-text"] + ) + ) diff --git a/src/masonite/notification/providers/NotificationProvider.py b/src/masonite/notification/providers/NotificationProvider.py new file mode 100644 index 000000000..0a740e7c3 --- /dev/null +++ b/src/masonite/notification/providers/NotificationProvider.py @@ -0,0 +1,41 @@ +from ...providers import Provider +from ...utils.structures import load +from ..drivers import ( + BroadcastDriver, + DatabaseDriver, + MailDriver, + SlackDriver, + VonageDriver, +) +from ...configuration import config + +from ..NotificationManager import NotificationManager +from ..MockNotification import MockNotification +from ..commands import MakeNotificationCommand, NotificationTableCommand + + +class NotificationProvider(Provider): + """Notifications Provider""" + + def __init__(self, application): + self.application = application + + def register(self): + notification_manager = NotificationManager(self.application).set_configuration( + config("notification") + ) + notification_manager.add_driver("mail", MailDriver(self.application)) + notification_manager.add_driver("vonage", VonageDriver(self.application)) + notification_manager.add_driver("slack", SlackDriver(self.application)) + notification_manager.add_driver("database", DatabaseDriver(self.application)) + notification_manager.add_driver("broadcast", BroadcastDriver(self.application)) + + self.application.bind("notification", notification_manager) + self.application.bind("mock.notification", MockNotification) + self.application.make("commands").add( + MakeNotificationCommand(self.application), + NotificationTableCommand(), + ) + + def boot(self): + pass diff --git a/src/masonite/notification/providers/__init__.py b/src/masonite/notification/providers/__init__.py new file mode 100644 index 000000000..acd9c98e9 --- /dev/null +++ b/src/masonite/notification/providers/__init__.py @@ -0,0 +1 @@ +from .NotificationProvider import NotificationProvider diff --git a/src/masonite/packages.py b/src/masonite/packages.py deleted file mode 100644 index 510ad6a46..000000000 --- a/src/masonite/packages.py +++ /dev/null @@ -1,139 +0,0 @@ -"""Third party package integrations.""" -import os -import shutil -import sys - - -def create_or_append_config(location, name=False): - if name: - file_name = name - else: - file_name = os.path.basename(location) - - # import it into the config directory - config_directory = os.path.join(os.getcwd(), "config") - - # if file does not exist - if not os.path.isfile(config_directory + "/" + file_name): - shutil.copyfile(location, config_directory + "/" + file_name) - print("\033[92mConfiguration File Created!\033[0m") - else: - # Append to the file - with open(config_directory + "/" + file_name, "a") as project_config, open( - location, "r" - ) as package_config: - project_config.write(package_config.read()) - - print("\033[92mConfiguration File Appended!\033[0m") - - -def append_web_routes(location): - # import it into the web.py file - routes_file = os.path.join(os.getcwd(), "routes/web.py") - - with open(routes_file, "a") as project_routes, open( - location, "r" - ) as package_routes: - project_routes.write(package_routes.read()) - - print("\033[92mroutes/web.py File Appended!\033[0m") - - -def append_file(from_location, to_location): - with open(from_location, "r") as from_file_pointer, open( - os.path.join(os.getcwd(), to_location), "a" - ) as to_file_pointer: - to_file_pointer.write(from_file_pointer.read()) - - print("\033[92m {} has been appended! \033[0m".format(to_location)) - - -def append_api_routes(location): - # import it into the web.py file - api_file = os.path.join(os.getcwd(), "routes/api.py") - - # Append to the file - with open(api_file, "a") as project_routes, open(location, "r") as package_routes: - project_routes.write(package_routes.read()) - - print("\033[92mroutes/api.py File Appended!\033[0m") - - -def create_controller(location, to="app/http/controllers"): - file_name = os.path.basename(location) - - controller_directory = os.path.join(os.getcwd(), to) - controller_file = os.path.join(controller_directory, file_name) - if not os.path.exists(controller_directory): - # Create the path to the model if it does not exist - os.makedirs(controller_directory) - - if os.path.isfile(controller_file): - # if file does exist - print("\033[91m{0} Controller Already Exists!\033[0m".format(file_name)) - else: - # copy controller over - shutil.copyfile(location, controller_file) - - print("\033[92m{0} Controller Created\033[0m".format(file_name)) - - -def add_venv_site_packages(): - try: - from config import packages - - # Add additional site packages to vendor if they exist - for directory in packages.SITE_PACKAGES: - path = os.path.join(os.getcwd(), directory) - sys.path.append(path) - except ImportError: - raise ImportError - - if "VIRTUAL_ENV" in os.environ: - python_version = None - venv_directory = os.listdir(os.path.join(os.environ["VIRTUAL_ENV"], "lib")) - - for directory in venv_directory: - if directory.startswith("python"): - python_version = directory - break - - if python_version: - site_packages_directory = os.path.join( - os.environ["VIRTUAL_ENV"], "lib", python_version, "site-packages" - ) - - sys.path.append(site_packages_directory) - else: - print( - "\033[93mWARNING: Could not add the virtual environment you are currently in. Attempting to add: {0}\033[93m".format( - os.environ["VIRTUAL_ENV"] - ) - ) - - -class PackageContainer: - def create(self): - from masonite.app import App - from config import providers - - container = App() - - container.bind("Container", container) - - container.bind("ProvidersConfig", providers) - container.bind("Providers", []) - container.bind("WSGIProviders", []) - - for provider in container.make("ProvidersConfig").PROVIDERS: - located_provider = provider() - located_provider.load_app(container).register() - if located_provider.wsgi: - container.make("WSGIProviders").append(located_provider) - else: - container.make("Providers").append(located_provider) - - for provider in container.make("Providers"): - container.resolve(provider.boot) - - return container diff --git a/src/masonite/packages/Package.py b/src/masonite/packages/Package.py new file mode 100644 index 000000000..51b6b4f8b --- /dev/null +++ b/src/masonite/packages/Package.py @@ -0,0 +1,46 @@ +import os + + +class Package: + def __init__(self): + self.root_dir = "" + self.name = "" + self.config = "" + self.commands = [] + self.views = [] + self.migrations = [] + self.controller_locations = [] + self.routes = [] + self.assets = [] + + def _build_path(self, rel_path): + return os.path.join(self.root_dir, rel_path) + + def add_config(self, config_path): + self.config = self._build_path(config_path) + return self + + def add_views(self, *locations): + for location in locations: + self.views.append(self._build_path(location)) + return self + + def add_migrations(self, *migrations): + for migration in migrations: + self.migrations.append(self._build_path(migration)) + return self + + def add_routes(self, *routes): + for route in routes: + self.routes.append(self._build_path(route)) + return self + + def add_assets(self, *assets): + for asset in assets: + self.assets.append(self._build_path(asset)) + return self + + def add_controller_locations(self, *controller_locations): + for loc in controller_locations: + self.controller_locations.append(self._build_path(loc)) + return self diff --git a/src/masonite/packages/PublishableResource.py b/src/masonite/packages/PublishableResource.py new file mode 100644 index 000000000..a4704e590 --- /dev/null +++ b/src/masonite/packages/PublishableResource.py @@ -0,0 +1,14 @@ +class PublishableResource: + def __init__(self, key): + self.key = key + self.files = [] + + def add(self, source, destination): + self.files.append((source, destination)) + return self + + # def add(self, *resources): + # for source, destination in resources: + # self.sources.append(source) + # self.destinations.append(destination) + # return self diff --git a/src/masonite/packages/__init__.py b/src/masonite/packages/__init__.py new file mode 100644 index 000000000..b3eeddb56 --- /dev/null +++ b/src/masonite/packages/__init__.py @@ -0,0 +1 @@ +from .providers import PackageProvider diff --git a/src/masonite/packages/providers/PackageProvider.py b/src/masonite/packages/providers/PackageProvider.py new file mode 100644 index 000000000..6724bd07b --- /dev/null +++ b/src/masonite/packages/providers/PackageProvider.py @@ -0,0 +1,176 @@ +import os +from collections import defaultdict +from os.path import relpath, join, abspath, basename, isdir, isfile +import shutil +from ...providers.Provider import Provider +from ...exceptions import InvalidPackageName +from ...utils.location import ( + base_path, + config_path, + views_path, + migrations_path, + resources_path, +) +from ...facades import Config +from ...utils.time import migration_timestamp +from ...routes import Route +from ...utils.structures import load + +from ..reserved_names import PACKAGE_RESERVED_NAMES +from ..Package import Package +from ..PublishableResource import PublishableResource + + +class PackageProvider(Provider): + + vendor_prefix = "vendor" + + def __init__(self, application): + self.application = application + # TODO: the default here could be set auto by deciding that its the dirname containing the provider ! + self.package = Package() + self.files = {} + self.default_resources = ["config", "views", "migrations", "assets"] + + def register(self): + self.configure() + + def boot(self): + pass + + # api + def configure(self): + pass + + def publish(self, resources, dry=False): + project_root = base_path() + resources_list = resources or self.default_resources + published_resources = defaultdict(lambda: []) + for resource in resources_list: + resource_files = self.files.get(resource, []) + for source, dest in resource_files: + if not dry: + shutil.copy(source, dest) + published_resources[resource].append(relpath(dest, project_root)) + return published_resources + + def root(self, abs_root_dir): + # TODO ensure abs path here! + self.package.root_dir = abs_root_dir + return self + + def name(self, name): + if name in PACKAGE_RESERVED_NAMES: + raise InvalidPackageName( + f"{name} is a reserved name. Please choose another name for your package." + ) + self.package.name = name + return self + + def vendor_name(self, name): + self.package.vendor_name = name + return self + + def config(self, config_filepath, publish=False): + # TODO: a name must be specified ! + self.package.add_config(config_filepath) + Config.merge_with(self.package.name, self.package.config) + if publish: + resource = PublishableResource("config") + resource.add(self.package.config, config_path(f"{self.package.name}.py")) + self.files.update({resource.key: resource.files}) + return self + + def views(self, *locations, publish=False): + """Register views location in the project. + locations must be a folder containinng the views you want to publish. + """ + self.package.add_views(*locations) + # register views into project + self.application.make("view").add_namespace( + self.package.name, self.package.views[0] + ) + + if publish: + resource = PublishableResource("views") + for location in self.package.views: + # views = get all files in this folder + for dirpath, _, filenames in os.walk(location): + for f in filenames: + resource.add( + abspath(join(dirpath, f)), + views_path( + join( + self.vendor_prefix, + self.package.name, + relpath(dirpath, location), + f, + ) + ), + ) + self.files.update({resource.key: resource.files}) + return self + + def commands(self, *commands): + self.application.make("commands").add(*commands) + return self + + def migrations(self, *migrations): + self.package.add_migrations(*migrations) + resource = PublishableResource("migrations") + for migration in self.package.migrations: + resource.add( + migration, + migrations_path(f"{migration_timestamp()}_{basename(migration)}"), + ) + self.files.update({resource.key: resource.files}) + return self + + def routes(self, *routes): + """Controller locations must have been loaded already !""" + self.package.add_routes(*routes) + for route_group in self.package.routes: + self.application.make("router").add( + Route.group( + load(route_group, "ROUTES", []), + ) + ) + return self + + def controllers(self, *controller_locations): + self.package.add_controller_locations(*controller_locations) + Route.add_controller_locations(*self.package.controller_locations) + return self + + def assets(self, *assets): + self.package.add_assets(*assets) + resource = PublishableResource("assets") + for asset_dir_or_file in self.package.assets: + # views = get all files in this folder + if isdir(asset_dir_or_file): + for dirpath, _, filenames in os.walk(asset_dir_or_file): + for f in filenames: + resource.add( + abspath(join(dirpath, f)), + resources_path( + join( + self.vendor_prefix, + self.package.name, + relpath(dirpath, asset_dir_or_file), + f, + ) + ), + ) + elif isfile(asset_dir_or_file): + resource.add( + abspath(asset_dir_or_file), + resources_path( + join( + self.vendor_prefix, + self.package.name, + asset_dir_or_file, + ) + ), + ) + self.files.update({resource.key: resource.files}) + return self diff --git a/src/masonite/packages/providers/__init__.py b/src/masonite/packages/providers/__init__.py new file mode 100644 index 000000000..a577a006a --- /dev/null +++ b/src/masonite/packages/providers/__init__.py @@ -0,0 +1 @@ +from .PackageProvider import PackageProvider diff --git a/src/masonite/packages/reserved_names.py b/src/masonite/packages/reserved_names.py new file mode 100644 index 000000000..52153d301 --- /dev/null +++ b/src/masonite/packages/reserved_names.py @@ -0,0 +1 @@ +PACKAGE_RESERVED_NAMES = ["application", "auth", "controller", "event", "notification"] diff --git a/src/masonite/pipeline/Pipeline.py b/src/masonite/pipeline/Pipeline.py new file mode 100644 index 000000000..afb2a0091 --- /dev/null +++ b/src/masonite/pipeline/Pipeline.py @@ -0,0 +1,11 @@ +class Pipeline: + def __init__(self, payload, *args): + self.payload = payload + self.args = args + + def through(self, pipe_list, handler="handle"): + passthrough = self.payload + for pipe in pipe_list: + response = getattr(pipe(), handler)(self.payload, *self.args) + if response != passthrough: + break diff --git a/src/masonite/pipeline/__init__.py b/src/masonite/pipeline/__init__.py new file mode 100644 index 000000000..1779011a5 --- /dev/null +++ b/src/masonite/pipeline/__init__.py @@ -0,0 +1 @@ +from .Pipeline import Pipeline diff --git a/app/providers/.gitignore b/src/masonite/pipeline/tasks/MiddlewareTask.py similarity index 100% rename from app/providers/.gitignore rename to src/masonite/pipeline/tasks/MiddlewareTask.py diff --git a/src/masonite/pipeline/tasks/ResponseTask.py b/src/masonite/pipeline/tasks/ResponseTask.py new file mode 100644 index 000000000..e4a873757 --- /dev/null +++ b/src/masonite/pipeline/tasks/ResponseTask.py @@ -0,0 +1,7 @@ +class ResponseTask: + def __init__(self): + pass + + def handle(self, request): + + return request diff --git a/src/masonite/provider.py b/src/masonite/provider.py deleted file mode 100644 index 41139520f..000000000 --- a/src/masonite/provider.py +++ /dev/null @@ -1,117 +0,0 @@ -"""Module for the Service Provider.""" - -from .helpers import random_string -from .helpers.filesystem import copy_migration -from .packages import append_file - - -class ServiceProvider: - """Service provider class. Used as mediator for loading objects or entire features into the container.""" - - wsgi = True - - def __init__(self): - """Service provider constructor.""" - self.app = None - self._publishes = {} - self._publish_tags = {} - - self._publish_migrations = [] - self._publish_migrations_tags = {} - - def boot(self): - """Use to boot things into the container. Typically ran after the register method has been ran.""" - pass - - def register(self): - """Use to register objects into the container.""" - pass - - def load_app(self, app): - """Load the container into the service provider. - - Arguments: - app {masonite.app.App} -- Container object. - - Returns: - self - """ - self.app = app - return self - - def routes(self, routes): - """Add routes to the container. - - Arguments: - routes {list} -- List of routes to add to the container - """ - web_routes = self.app.make("WebRoutes") - web_routes += routes - - def http_middleware(self, middleware): - """Add HTTP middleware to the container. - - Arguments: - middleware {list} -- List of middleware to add - """ - http_middleware = self.app.make("HttpMiddleware") - http_middleware += middleware - - def route_middleware(self, middleware): - """Add route middleware to the container. - - Arguments: - middleware {dict} -- A dictionary of route middleware to add - """ - route_middleware = self.app.make("RouteMiddleware") - route_middleware.update(middleware) - - def migrations(self, *directories): - """Add migration directories to the container.""" - for directory in directories: - self.app.bind("{}_MigrationDirectory".format(random_string(4)), directory) - - def commands(self, *commands): - """Add commands to the container. Pass in the commands as arguments.""" - for command in commands: - self.app.bind( - "{}Command".format(command.__class__.__name__.replace("Command", "")), - command, - ) - - def assets(self, assets): - """Add assets to the container. - - Arguments: - assets {dict} -- A dictionary of assets to add - """ - - self.app.make("staticfiles").update(assets) - - def publishes(self, dictionary, tag=None): - self._publishes.update(dictionary) - if tag is not None: - self._publish_tags.update({tag: dictionary}) - - def publishes_migrations(self, migrations, tag=None): - self._publish_migrations += migrations - if tag is not None: - self._publish_migrations_tags.update({tag: migrations}) - - def publish(self, tag=None): - if tag is not None: - publishing_items = self._publish_tags.get(tag) - else: - publishing_items = self._publishes - - for from_location, to_location in publishing_items.items(): - append_file(from_location, to_location) - - def publish_migrations(self, tag=None): - if tag is not None: - publishing_items = self._publish_migrations_tags.get(tag) - else: - publishing_items = self._publish_migrations - - for from_location in publishing_items: - copy_migration(from_location) diff --git a/src/masonite/providers/AppProvider.py b/src/masonite/providers/AppProvider.py deleted file mode 100644 index 986d546ba..000000000 --- a/src/masonite/providers/AppProvider.py +++ /dev/null @@ -1,93 +0,0 @@ -"""An AppProvider Service Provider.""" - -from ..autoload import Autoload -from ..commands import ( - AuthCommand, - CommandCommand, - ControllerCommand, - DownCommand, - InfoCommand, - JobCommand, - KeyCommand, - MailableCommand, - MiddlewareCommand, - ModelCommand, - ModelDocstringCommand, - ProviderCommand, - PublishCommand, - PresetCommand, - QueueTableCommand, - QueueWorkCommand, - RoutesCommand, - ServeCommand, - TestCommand, - TinkerCommand, - UpCommand, - ViewCommand, -) -from ..exception_handler import DumpHandler, ExceptionHandler -from ..helpers import config, load -from ..helpers.routes import flatten_routes -from ..hook import Hook -from ..provider import ServiceProvider -from ..request import Request -from ..response import Response -from ..routes import Route - - -class AppProvider(ServiceProvider): - - wsgi = True - - def register(self): - self.app.bind("HookHandler", Hook(self.app)) - self.app.bind("WebRoutes", flatten_routes(load("routes.web.routes"))) - self.app.bind("Route", Route()) - - self.app.bind("Container", self.app) - - self.app.bind("ExceptionDumpExceptionHandler", DumpHandler) - - self.app.bind("RouteMiddleware", config("middleware.route_middleware")) - self.app.bind("HttpMiddleware", config("middleware.http_middleware")) - self.app.bind("staticfiles", config("storage.staticfiles", {})) - - # Insert Commands - self._load_commands() - - self._autoload(config("application.autoload")) - - def boot(self, route: Route): - self.app.bind("Request", Request(self.app.make("Environ")).load_app(self.app)) - self.app.simple(Response(self.app)) - route.load_environ(self.app.make("Environ")) - self.app.bind("ExceptionHandler", ExceptionHandler(self.app)) - - def _autoload(self, directories): - Autoload(self.app).load(directories) - - def _load_commands(self): - self.commands( - AuthCommand(), - CommandCommand(), - ControllerCommand(), - DownCommand(), - InfoCommand(), - JobCommand(), - KeyCommand(), - MailableCommand(), - MiddlewareCommand(), - ModelCommand(), - ModelDocstringCommand(), - PresetCommand(), - ProviderCommand(), - PublishCommand(), - QueueWorkCommand(), - QueueTableCommand(), - ViewCommand(), - RoutesCommand(), - ServeCommand(), - TestCommand(), - TinkerCommand(), - UpCommand(), - ) diff --git a/src/masonite/providers/AuthenticationProvider.py b/src/masonite/providers/AuthenticationProvider.py index 88b592176..3e5ba0a5e 100644 --- a/src/masonite/providers/AuthenticationProvider.py +++ b/src/masonite/providers/AuthenticationProvider.py @@ -1,20 +1,21 @@ -"""An Authentication Service Provider.""" +from ..foundation import response_handler +from ..request import Request +from ..response import Response +from ..authentication import Auth +from ..authentication.guards import WebGuard +from ..configuration import config +from .Provider import Provider -from ..auth.guards import Guard, WebGuard -from ..auth import Auth -from ..helpers import config -from ..provider import ServiceProvider - -class AuthenticationProvider(ServiceProvider): - - wsgi = False +class AuthenticationProvider(Provider): + def __init__(self, application): + self.application = application def register(self): - guard = Guard(self.app) - guard.register_guard("web", WebGuard) - self.app.simple(guard) - self.app.swap(Auth, guard) + auth = Auth(self.application).set_configuration(config("auth.guards")) + auth.add_guard("web", WebGuard(self.application)) + + self.application.bind("auth", auth) - def boot(self, auth: Auth): - auth.set(config("auth.auth.defaults.guard")) + def boot(self): + pass diff --git a/src/masonite/providers/AuthorizationProvider.py b/src/masonite/providers/AuthorizationProvider.py new file mode 100644 index 000000000..dde3a101e --- /dev/null +++ b/src/masonite/providers/AuthorizationProvider.py @@ -0,0 +1,13 @@ +from ..authorization import Gate +from .Provider import Provider + + +class AuthorizationProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + self.application.bind("gate", Gate(self.application)) + + def boot(self): + pass diff --git a/src/masonite/providers/BroadcastProvider.py b/src/masonite/providers/BroadcastProvider.py deleted file mode 100644 index fa6a64128..000000000 --- a/src/masonite/providers/BroadcastProvider.py +++ /dev/null @@ -1,28 +0,0 @@ -"""A RedirectionProvider Service Provider.""" - -from ..drivers import BroadcastAblyDriver, BroadcastPusherDriver, BroadcastPubNubDriver -from ..managers import BroadcastManager -from ..provider import ServiceProvider -from .. import Broadcast -from ..helpers import config - - -class BroadcastProvider(ServiceProvider): - - wsgi = False - - def register(self): - self.app.bind("BroadcastPusherDriver", BroadcastPusherDriver) - self.app.bind("BroadcastAblyDriver", BroadcastAblyDriver) - self.app.bind("BroadcastPubnubDriver", BroadcastPubNubDriver) - self.app.bind("BroadcastManager", BroadcastManager(self.app)) - - def boot(self): - self.app.bind( - "Broadcast", - self.app.make("BroadcastManager").driver(config("broadcast.driver")), - ) - self.app.swap( - Broadcast, - self.app.make("BroadcastManager").driver(config("broadcast.driver")), - ) diff --git a/src/masonite/providers/CacheProvider.py b/src/masonite/providers/CacheProvider.py index 8b1067911..7de4ce9e4 100644 --- a/src/masonite/providers/CacheProvider.py +++ b/src/masonite/providers/CacheProvider.py @@ -1,23 +1,19 @@ -"""A Cache Service Provider.""" +from .Provider import Provider +from ..cache import Cache +from ..cache.drivers import FileDriver, RedisDriver, MemcacheDriver +from ..configuration import config -from .. import Cache -from ..drivers import CacheDiskDriver, CacheRedisDriver -from ..managers import CacheManager -from ..provider import ServiceProvider -from ..helpers import config - -class CacheProvider(ServiceProvider): - - wsgi = False +class CacheProvider(Provider): + def __init__(self, application): + self.application = application def register(self): - # from config import cache - # self.app.bind('CacheConfig', cache) - self.app.bind("CacheDiskDriver", CacheDiskDriver) - self.app.bind("CacheRedisDriver", CacheRedisDriver) - self.app.bind("CacheManager", CacheManager(self.app)) + cache = Cache(self.application).set_configuration(config("cache.stores")) + cache.add_driver("file", FileDriver(self.application)) + cache.add_driver("redis", RedisDriver(self.application)) + cache.add_driver("memcache", MemcacheDriver(self.application)) + self.application.bind("cache", cache) - def boot(self, cache: CacheManager): - self.app.bind("Cache", cache.driver(config("cache").DRIVER)) - self.app.swap(Cache, cache.driver(config("cache").DRIVER)) + def boot(self): + pass diff --git a/src/masonite/providers/CorsProvider.py b/src/masonite/providers/CorsProvider.py deleted file mode 100644 index d4c8c351d..000000000 --- a/src/masonite/providers/CorsProvider.py +++ /dev/null @@ -1,22 +0,0 @@ -from ..provider import ServiceProvider -from ..request import Request -from ..response import Response -from ..helpers import config - - -class CorsProvider(ServiceProvider): - """Provides Services To The Service Container.""" - - wsgi = True - - def register(self): - """Register objects into the Service Container.""" - pass - - def boot(self, request: Request, response: Response): - """Boots services required by the container.""" - headers = config("middleware.cors") or {} - response.header(headers) - - if request.get_request_method().lower() == "options": - response.view("preflight") diff --git a/src/masonite/providers/CsrfProvider.py b/src/masonite/providers/CsrfProvider.py deleted file mode 100644 index 029fce8ae..000000000 --- a/src/masonite/providers/CsrfProvider.py +++ /dev/null @@ -1,16 +0,0 @@ -"""A Csrf Service Provider.""" - -from ..auth import Csrf -from ..provider import ServiceProvider - - -class CsrfProvider(ServiceProvider): - - wsgi = True - - def register(self): - pass - - def boot(self): - self.app.bind("Csrf", Csrf(self.app.make("Request"))) - pass diff --git a/src/masonite/providers/ExceptionProvider.py b/src/masonite/providers/ExceptionProvider.py new file mode 100644 index 000000000..b977a1d33 --- /dev/null +++ b/src/masonite/providers/ExceptionProvider.py @@ -0,0 +1,21 @@ +import builtins + +from .Provider import Provider +from ..exceptions import ExceptionHandler, DumpExceptionHandler, DD +from ..configuration import config + + +class ExceptionProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + handler = ExceptionHandler(self.application).set_options(config("exceptions")) + builtins.dd = DD(self.application).dump + self.application.bind("exception_handler", handler) + self.application.bind( + "DumpExceptionHandler", DumpExceptionHandler(self.application) + ) + + def boot(self): + pass diff --git a/src/masonite/providers/FrameworkProvider.py b/src/masonite/providers/FrameworkProvider.py new file mode 100644 index 000000000..723b9bb32 --- /dev/null +++ b/src/masonite/providers/FrameworkProvider.py @@ -0,0 +1,17 @@ +from ..foundation import response_handler +from ..request import Request +from ..response import Response + + +class FrameworkProvider: + def __init__(self, application): + self.application = application + + def register(self): + pass + + def boot(self): + request = Request(self.application.make("environ")) + request.app = self.application + self.application.bind("request", request) + self.application.bind("response", Response(self.application)) diff --git a/src/masonite/providers/HashServiceProvider.py b/src/masonite/providers/HashServiceProvider.py new file mode 100644 index 000000000..b575b3b08 --- /dev/null +++ b/src/masonite/providers/HashServiceProvider.py @@ -0,0 +1,20 @@ +from ..hashing import Hash +from ..hashing.drivers import BcryptHasher, Argon2Hasher +from .Provider import Provider +from ..configuration import config + + +class HashServiceProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + hashing = Hash(self.application).set_configuration( + config("application.hashing") + ) + hashing.add_driver("bcrypt", BcryptHasher()) + hashing.add_driver("argon2", Argon2Hasher()) + self.application.bind("hash", hashing) + + def boot(self): + pass diff --git a/src/masonite/providers/HelpersProvider.py b/src/masonite/providers/HelpersProvider.py index ee544514d..fed521133 100644 --- a/src/masonite/providers/HelpersProvider.py +++ b/src/masonite/providers/HelpersProvider.py @@ -1,55 +1,39 @@ -"""A Helpers Service Provider.""" - import builtins -import os - -from ..exception_handler import DD -from ..helpers.view_helpers import back, set_request_method, hidden, old -from ..helpers.sign import sign, unsign, decrypt, encrypt -from ..helpers import config, optional -from ..provider import ServiceProvider -from ..view import View -from ..request import Request -from ..managers import MailManager +from markupsafe import Markup +from ..providers import Provider +from ..configuration import config +from ..helpers import UrlsHelper, MixHelper -class HelpersProvider(ServiceProvider): - wsgi = False +class HelpersProvider(Provider): + def __init__(self, application): + self.application = application def register(self): - pass + builtins.resolve = self.application.resolve + builtins.container = lambda: self.application + self.application.bind("url", UrlsHelper(self.application)) - def boot(self, view: View): - """Add helper functions to Masonite.""" - builtins.view = view.render - # builtins.auth = request.user - builtins.container = self.app.helper - builtins.env = os.getenv - builtins.resolve = self.app.resolve - # builtins.route = request.route - if self.app.has(MailManager): - builtins.mail_helper = self.app.make(MailManager).helper - builtins.dd = DD(self.app).dump + def boot(self): + request = self.application.make("request") + urls_helper = self.application.make("url") - view.share( + self.application.make("view").share( { - # "request": request.helper, - # "auth": request.user, - "request_method": set_request_method, - # "route": request.route, - "back": back, - "sign": sign, - "unsign": unsign, - "decrypt": decrypt, - "encrypt": encrypt, + "request": lambda: request, + "session": lambda: request.app.make("session"), + "auth": request.user, + "cookie": request.cookie, + "back": lambda url=request.get_path(): ( + Markup(f"") + ), + "asset": urls_helper.asset, + "url": urls_helper.url, + "mix": MixHelper(self.application).url, + "route": urls_helper.route, "config": config, - "optional": optional, - "dd": builtins.dd, - "hidden": hidden, - "exists": view.exists, - # "cookie": request.get_cookie, - "url": lambda name, params={}: request.route(name, params, full=True), - "old": old, + "can": self.application.make("gate").allows, + "cannot": self.application.make("gate").denies, } ) diff --git a/src/masonite/providers/MailProvider.py b/src/masonite/providers/MailProvider.py index d6fa80fb6..319754788 100644 --- a/src/masonite/providers/MailProvider.py +++ b/src/masonite/providers/MailProvider.py @@ -1,28 +1,22 @@ -"""A Mail Service Provider.""" +from .Provider import Provider +from ..mail import Mail +from ..mail.drivers import SMTPDriver, TerminalDriver, MailgunDriver +from ..utils.structures import load +from ..mail import MockMail +from ..facades import Config -from ..drivers import ( - MailMailgunDriver, - MailSmtpDriver, - MailLogDriver, - MailTerminalDriver, -) -from ..managers import MailManager -from ..provider import ServiceProvider -from .. import Mail -from ..helpers import config - -class MailProvider(ServiceProvider): - - wsgi = False +class MailProvider(Provider): + def __init__(self, application): + self.application = application def register(self): - self.app.bind("MailSmtpDriver", MailSmtpDriver) - self.app.bind("MailMailgunDriver", MailMailgunDriver) - self.app.bind("MailLogDriver", MailLogDriver) - self.app.bind("MailTerminalDriver", MailTerminalDriver) - self.app.bind("MailManager", MailManager(self.app)) + mail = Mail(self.application).set_configuration(Config.get("mail.drivers")) + mail.add_driver("smtp", SMTPDriver(self.application)) + mail.add_driver("mailgun", MailgunDriver(self.application)) + mail.add_driver("terminal", TerminalDriver(self.application)) + self.application.bind("mail", mail) + self.application.bind("mock.mail", MockMail) - def boot(self, manager: MailManager): - self.app.bind("Mail", manager.driver(config("mail.driver"))) - self.app.swap(Mail, manager.driver(config("mail.driver"))) + def boot(self): + pass diff --git a/src/masonite/providers/Provider.py b/src/masonite/providers/Provider.py new file mode 100644 index 000000000..665a0b50b --- /dev/null +++ b/src/masonite/providers/Provider.py @@ -0,0 +1,2 @@ +class Provider: + pass diff --git a/src/masonite/providers/QueueProvider.py b/src/masonite/providers/QueueProvider.py index 0f9c7e509..f30c19c5d 100644 --- a/src/masonite/providers/QueueProvider.py +++ b/src/masonite/providers/QueueProvider.py @@ -1,23 +1,18 @@ -"""A RedirectionProvider Service Provider.""" +from ..drivers.queue import DatabaseDriver, AsyncDriver, AMQPDriver +from ..queues import Queue +from ..configuration import config -from ..drivers import QueueAsyncDriver, QueueAmqpDriver, QueueDatabaseDriver -from ..managers import QueueManager -from ..provider import ServiceProvider -from .. import Queue -from ..helpers import config - - -class QueueProvider(ServiceProvider): - - wsgi = False +class QueueProvider: + def __init__(self, application): + self.application = application def register(self): - self.app.bind("QueueAsyncDriver", QueueAsyncDriver) - self.app.bind("QueueAmqpDriver", QueueAmqpDriver) - self.app.bind("QueueDatabaseDriver", QueueDatabaseDriver) - self.app.bind("QueueManager", QueueManager) + queue = Queue(self.application).set_configuration(config("queue.drivers")) + queue.add_driver("database", DatabaseDriver(self.application)) + queue.add_driver("async", AsyncDriver(self.application)) + queue.add_driver("amqp", AMQPDriver(self.application)) + self.application.bind("queue", queue) - def boot(self, queue: QueueManager): - self.app.bind("Queue", queue.driver(config("queue.driver"))) - self.app.swap(Queue, queue.driver(config("queue.driver"))) + def boot(self): + pass diff --git a/src/masonite/providers/RequestHelpersProviders.py b/src/masonite/providers/RequestHelpersProviders.py deleted file mode 100644 index e49db49df..000000000 --- a/src/masonite/providers/RequestHelpersProviders.py +++ /dev/null @@ -1,28 +0,0 @@ -"""A Helpers Service Provider.""" - -import builtins -import os - -from ..provider import ServiceProvider -from ..view import View -from ..request import Request - - -class RequestHelpersProvider(ServiceProvider): - def register(self): - pass - - def boot(self, view: View, request: Request): - """Add helper functions to Masonite.""" - builtins.auth = request.user - builtins.route = request.route - - view.share( - { - "request": request.helper, - "auth": request.user, - "route": request.route, - "cookie": request.get_cookie, - "url": lambda name, params={}: request.route(name, params, full=True), - } - ) diff --git a/src/masonite/providers/RouteProvider.py b/src/masonite/providers/RouteProvider.py index 2ab036135..364ef6b67 100644 --- a/src/masonite/providers/RouteProvider.py +++ b/src/masonite/providers/RouteProvider.py @@ -1,124 +1,66 @@ -"""A RouteProvider Service Provider.""" +from inspect import isclass -from ..helpers.routes import create_matchurl -from ..provider import ServiceProvider -from ..request import Request from ..response import Response +from ..facades import Response as ResponseFacade +from .Provider import Provider from ..routes import Route +from ..pipeline import Pipeline -class RouteProvider(ServiceProvider): - def register(self): - pass - - def boot(self, router: Route, request: Request, response: Response): - # All routes joined - from config import application - - for route in self.app.make("WebRoutes"): - - """Make a better match for trailing slashes - Sometimes a user will end with a trailing slash. Because the user might - create routes like `/url/route` and `/url/route/` and how the regex - is compiled down, we may need to adjust for urls that end or dont - end with a trailing slash. - """ - - matchurl = create_matchurl(router.url, route) - - """Houston, we've got a match - Check to see if a route matches the corresponding router url. If a match - is found, execute that route and break out of the loop. We only need - one match. Routes are executed on a first come, first serve basis - """ - if ( - matchurl - and matchurl.match(router.url) - and request.get_request_method() in route.method_type - ): - route.load_request(request) +class RouteProvider(Provider): + def __init__(self, application): + self.application = application - """Check if subdomains are active and if the route matches on the subdomain - It needs to match to. - """ - - if request.has_subdomain(): - # Check if the subdomain matches the correct routes domain - if not route.has_required_domain(): - continue - - """Get URL Parameters - This will create a dictionary of parameters given. This is sort of a short - but complex way to retrieve the url parameters. - This is the code used to convert /url/@firstname/@lastname - to {'firstmane': 'joseph', 'lastname': 'mancuso'}. - """ - - try: - parameter_dict = {} - for index, value in enumerate(matchurl.match(router.url).groups()): - parameter_dict[ - route.url_list[index] - ] = value or route.get_default_parameter(route.url_list[index]) - request.set_params(parameter_dict) - except AttributeError: + def register(self): + # Register the routes? + Route.set_controller_locations(self.application.make("controllers.location")) + + def boot(self): + router = self.application.make("router") + request = self.application.make("request") + response = self.application.make("response") + + route = router.find( + request.get_path(), request.get_request_method(), request.get_subdomain() + ) + + # Run before middleware + + Pipeline(request, response).through( + self.application.make("middleware").get_http_middleware(), + handler="before", + ) + + exception = None + + if route: + request.load_params(route.extract_parameters(request.get_path())) + self.application.make("middleware").run_route_middleware( + route.list_middleware, request, response, callback="before" + ) + + try: + data = route.get_response(self.application) + if isinstance(data, Response) or ( + isclass(data) and issubclass(data, ResponseFacade) + ): pass + else: + response.view(data) + except Exception as e: + exception = e - """Excute HTTP before middleware - Only those middleware that have a "before" method are ran. - """ - - for http_middleware in self.app.make("HttpMiddleware"): - located_middleware = self.app.resolve(http_middleware) - if hasattr(located_middleware, "before"): - located_middleware.before() + self.application.make("middleware").run_route_middleware( + route.list_middleware, request, response, callback="after" + ) - """Execute Route Before Middleware - This is middleware that contains a before method. - """ - - route.run_middleware("before") - - # Show a helper in the terminal of which route has been visited - if application.DEBUG: - print(request.get_request_method() + " Route: " + router.url) - - # If no routes have been found and no middleware has changed the status code - if not response.get_status_code(): - - """Get the response from the route and set it on the 'Response' key. - This data is typically the output of the controller method depending - on the type of route. - """ - response.view(route.get_response(), status=200) - - """Execute Route After Route Middleware - This is middleware that contains an after method. - """ - - route.run_middleware("after") - - """Excute HTTP after middleware - Only those middleware that have an "after" method are ran. - Check here if the middleware even has the required method. - """ - - for http_middleware in self.app.make("HttpMiddleware"): - located_middleware = self.app.resolve(http_middleware) - - if hasattr(located_middleware, "after"): - located_middleware.after() + else: + response.view("route not found", status=404) - """Return breaks the loop because the incoming route is found and executed. - There is no need to continue searching the route list. First come - first serve around these parts of the woods. - """ - return + Pipeline(request, response).through( + self.application.make("middleware").get_http_middleware(), + handler="after", + ) - """No Response was found in the for loop so let's set an arbitrary response now. - """ - # If the route exists but not the method is incorrect - if request.route_exists(request.path): - response.view("Method not allowed", status=405) - else: - response.view("Route not found. Error 404", status=404) + if exception: + raise exception diff --git a/src/masonite/providers/SessionProvider.py b/src/masonite/providers/SessionProvider.py index 341b9342e..0108eac72 100644 --- a/src/masonite/providers/SessionProvider.py +++ b/src/masonite/providers/SessionProvider.py @@ -1,25 +1,22 @@ -"""A RedirectionProvider Service Provider.""" +from .Provider import Provider +from ..sessions import Session +from ..drivers.session import CookieDriver +from ..utils.structures import load +from ..configuration import config -from ..drivers import SessionCookieDriver, SessionMemoryDriver -from ..managers import SessionManager -from ..provider import ServiceProvider -from ..view import View -from ..request import Request -from .. import Session -from ..helpers import config +class SessionProvider(Provider): + def __init__(self, application): + self.application = application -class SessionProvider(ServiceProvider): def register(self): - # from config import session - # self.app.bind('SessionConfig', session) - self.app.bind("SessionMemoryDriver", SessionMemoryDriver) - self.app.bind("SessionCookieDriver", SessionCookieDriver) - self.app.bind("SessionManager", SessionManager(self.app)) + session = Session(self.application).set_configuration(config("session.drivers")) + session.add_driver("cookie", CookieDriver(self.application)) + self.application.bind("session", session) + self.application.make("view").share({"old": self.old}) - def boot(self, request: Request, view: View, session: SessionManager): - self.app.bind("Session", session.driver(config("session").DRIVER)) - self.app.swap(Session, session.driver(config("session").DRIVER)) - request.session = self.app.make("Session") + def boot(self): + pass - view.share({"session": self.app.make("Session").helper}) + def old(self, key): + return self.application.make("session").get(key) or "" diff --git a/src/masonite/providers/StatusCodeProvider.py b/src/masonite/providers/StatusCodeProvider.py deleted file mode 100644 index ae715049b..000000000 --- a/src/masonite/providers/StatusCodeProvider.py +++ /dev/null @@ -1,68 +0,0 @@ -"""A StatusProvider Service Provider.""" - -import json - -from ..helpers import config -from ..provider import ServiceProvider -from ..response import Response - - -class ServerErrorExceptionHook: - def load(self, app): - from config import application - - if application.DEBUG: - return - - response = app.make(Response) - - response.status(500) - if app.make("ViewClass").exists("errors/500"): - rendered_view = app.make("View")("errors/500") - else: - rendered_view = app.make("View")( - config( - "application.templates.statuscode", "/masonite/snippets/statuscode" - ), - {"code": "500 Internal Server Error"}, - ) - - response.view(rendered_view) - - -class StatusCodeProvider(ServiceProvider): - def register(self): - self.app.bind("ServiceErrorExceptionHook", ServerErrorExceptionHook()) - - def boot(self): - request = self.app.make("Request") - response = self.app.make(Response) - if response.is_status(200): - return - - if response.get_status() in (500, 405, 404): - if "application/json" in request.header("Content-Type"): - # Returns json response when we want the client to receive a json response - body = json.loads(self.app.make("Response").decode("utf-8")) - json_response = { - "error": {"status": response.get_status(), "body": body} - } - response.view(json_response, status=response.get_status()) - else: - # Returns html response when json is not explicitly specified - if self.app.make("ViewClass").exists( - "errors/{}".format(response.get_status()) - ): - rendered_view = self.app.make("View")( - "errors/{}".format(response.get_status()) - ) - else: - rendered_view = self.app.make("View")( - config( - "application.templates.statuscode", - "/masonite/snippets/statuscode", - ), - {"code": response.get_status_code()}, - ) - - response.view(rendered_view, status=response.get_status()) diff --git a/src/masonite/providers/UploadProvider.py b/src/masonite/providers/UploadProvider.py deleted file mode 100644 index be8080ae1..000000000 --- a/src/masonite/providers/UploadProvider.py +++ /dev/null @@ -1,26 +0,0 @@ -"""An Upload Service Provider.""" - -from ..drivers import UploadDiskDriver, UploadS3Driver -from ..helpers.static import static -from ..managers import UploadManager -from ..provider import ServiceProvider -from ..view import View -from .. import Upload -from ..helpers import config - - -class UploadProvider(ServiceProvider): - - wsgi = False - - def register(self): - # from config import storage - # self.app.bind('StorageConfig', storage) - self.app.bind("UploadDiskDriver", UploadDiskDriver) - self.app.bind("UploadS3Driver", UploadS3Driver) - self.app.bind("UploadManager", UploadManager(self.app)) - - def boot(self, manager: UploadManager, view: View): - self.app.bind("Upload", manager.driver(config("storage").DRIVER)) - self.app.swap(Upload, manager.driver(config("storage").DRIVER)) - view.share({"static": static}) diff --git a/src/masonite/providers/ViewProvider.py b/src/masonite/providers/ViewProvider.py index a20ef8c61..15d09ef68 100644 --- a/src/masonite/providers/ViewProvider.py +++ b/src/masonite/providers/ViewProvider.py @@ -1,20 +1,16 @@ -"""A View Service Provider.""" +from ..views import View +from .Provider import Provider -from jinja2 import FileSystemLoader -from ..provider import ServiceProvider -from ..view import View - - -class ViewProvider(ServiceProvider): - - wsgi = False +class ViewProvider(Provider): + def __init__(self, app): + self.application = app def register(self): - view = View(self.app) - self.app.bind("ViewClass", view) - self.app.bind("View", view.render) + view = View(self.application) + view.add_location(self.application.make("views.location")) + + self.application.bind("view", view) - def boot(self, view: View): - view.add_environment("src/masonite/snippets", loader=FileSystemLoader) - self.publishes_migrations(["storage/append_from.txt"]) + def boot(self): + pass diff --git a/src/masonite/providers/WhitenoiseProvider.py b/src/masonite/providers/WhitenoiseProvider.py index 985c78e10..92511d97a 100644 --- a/src/masonite/providers/WhitenoiseProvider.py +++ b/src/masonite/providers/WhitenoiseProvider.py @@ -1,30 +1,26 @@ -"""A WhiteNoiseProvider Service Provider.""" - +from .Provider import Provider from whitenoise import WhiteNoise - -from ..provider import ServiceProvider -from ..helpers import config +import os -class WhitenoiseProvider(ServiceProvider): - - wsgi = False +class WhitenoiseProvider(Provider): + def __init__(self, application): + self.application = application def register(self): - pass - - def boot(self): - """Wrap the WSGI server in a whitenoise container.""" - from config import application - self.app.bind( - "WSGI", - WhiteNoise( - self.app.make("WSGI"), - root=config("application.static_root"), - autorefresh=application.DEBUG, - ), + response_handler = WhiteNoise( + self.application.get_response_handler(), + root=self.application.get_storage_path(), + autorefresh=True, ) - for location, alias in self.app.make("staticfiles").items(): - self.app.make("WSGI").add_files(location, prefix=alias) + for location, alias in ( + self.application.make("storage_capsule").get_storage_assets().items() + ): + response_handler.add_files(location, prefix=alias) + + self.application.set_response_handler(response_handler) + + def boot(self): + return diff --git a/src/masonite/providers/__init__.py b/src/masonite/providers/__init__.py index 34caf2d36..3629c3e7d 100644 --- a/src/masonite/providers/__init__.py +++ b/src/masonite/providers/__init__.py @@ -1,16 +1,21 @@ -from .AppProvider import AppProvider -from .AuthenticationProvider import AuthenticationProvider -from .BroadcastProvider import BroadcastProvider -from .CacheProvider import CacheProvider -from .CsrfProvider import CsrfProvider -from .CorsProvider import CorsProvider -from .HelpersProvider import HelpersProvider -from .MailProvider import MailProvider -from .QueueProvider import QueueProvider +from .FrameworkProvider import FrameworkProvider from .RouteProvider import RouteProvider -from .SessionProvider import SessionProvider -from .UploadProvider import UploadProvider from .ViewProvider import ViewProvider -from .RequestHelpersProviders import RequestHelpersProvider from .WhitenoiseProvider import WhitenoiseProvider -from .StatusCodeProvider import StatusCodeProvider +from .ExceptionProvider import ExceptionProvider +from .AuthenticationProvider import AuthenticationProvider +from .AuthorizationProvider import AuthorizationProvider +from .Provider import Provider +from .MailProvider import MailProvider +from .SessionProvider import SessionProvider +from .HelpersProvider import HelpersProvider +from .QueueProvider import QueueProvider +from .CacheProvider import CacheProvider +from ..events.providers import EventProvider +from ..filesystem.providers import StorageProvider +from ..broadcasting.providers import BroadcastProvider +from ..scheduling.providers import ScheduleProvider +from ..essentials.providers.HashIDProvider import HashIDProvider +from .HashServiceProvider import HashServiceProvider +from ..validation.providers import ValidationProvider +from ..configuration.providers import ConfigurationProvider diff --git a/src/masonite/queues/Queue.py b/src/masonite/queues/Queue.py new file mode 100644 index 000000000..3bb42b643 --- /dev/null +++ b/src/masonite/queues/Queue.py @@ -0,0 +1,45 @@ +class Queue: + def __init__(self, application, driver_config=None): + self.application = application + self.drivers = {} + self.driver_config = driver_config or {} + self.options = {} + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + + def set_configuration(self, config): + self.driver_config = config + return self + + def get_driver(self, name=None): + if name is None: + return self.drivers[self.driver_config.get("default")] + return self.drivers[name] + + def get_config_options(self, driver=None): + if driver is None: + return self.driver_config.get(self.driver_config.get("default"), {}) + + return self.driver_config.get(driver, {}) + + def push(self, *jobs, **options): + driver = self.get_driver(options.get("driver")) + config_options = self.get_config_options(options.get("driver")) + config_options.update({"queue": options.get("queue", "default")}) + driver.set_options(config_options) + driver.push(*jobs) + + def consume(self, options): + driver = self.get_driver(options.get("driver")) + config_options = self.get_config_options(options.get("driver")) + config_options.update(options) + options.update(self.get_config_options(options.get("driver"))) + return driver.set_options(config_options).consume() + + def retry(self, options): + driver = self.get_driver(options.get("driver")) + config_options = self.get_config_options(options.get("driver")) + config_options.update(options) + options.update(self.get_config_options(options.get("driver"))) + return driver.set_options(config_options).retry() diff --git a/src/masonite/queues/Queueable.py b/src/masonite/queues/Queueable.py index a81c53dd9..040165e8c 100644 --- a/src/masonite/queues/Queueable.py +++ b/src/masonite/queues/Queueable.py @@ -8,16 +8,10 @@ class Queueable: run_times = 3 def handle(self): - """Put the queue logic in this handle method.""" pass - def dispatch(self): - """Responsible for dispatching the job to the Queue service. - - Returns: - self.handle - """ - return self.handle + def failed(self, obj, e): + pass - def should_run(self, job): - return True + def __repr__(self): + return self.__class__.__name__ diff --git a/src/masonite/queues/__init__.py b/src/masonite/queues/__init__.py index 74ed50308..8452f1aa1 100644 --- a/src/masonite/queues/__init__.py +++ b/src/masonite/queues/__init__.py @@ -1,2 +1,3 @@ from .Queueable import Queueable from .ShouldQueue import ShouldQueue +from .Queue import Queue diff --git a/src/masonite/request.py b/src/masonite/request.py deleted file mode 100644 index de6ca4f22..000000000 --- a/src/masonite/request.py +++ /dev/null @@ -1,1156 +0,0 @@ -"""Request Module. - -Request Module handles many different aspects of a single request -Methods which require the request and help ease development should -be put here. - -Methods may return another object if necessary to expand capabilities -of this class. -""" - -import re -import cgi -import json -from cgi import MiniFieldStorage -from http import cookies -from urllib.parse import parse_qs, quote - -import tldextract -from cryptography.fernet import InvalidToken -from .auth.Sign import Sign -from .exceptions import InvalidHTTPStatusCode, RouteException -from .helpers import Dot as DictDot -from .helpers import clean_request_input, dot -from .helpers.Extendable import Extendable -from .helpers.routes import compile_route_to_regex, query_parse -from .helpers.status import response_statuses -from .helpers.time import cookie_expire_time -from .cookies import CookieJar -from .headers import HeaderBag, Header -from .response import Response - - -class Request(Extendable): - """Handles many different aspects of a single request. - - This is the object passed through to the controllers - as a request parameter - - Arguments: - Extendable {masonite.helpers.Extendable.Extendable} -- Makes this class - have the ability to extend another class at runtime. - """ - - def __init__(self, environ=None): - """Request class constructor. - - Initializes several properties and sets various methods - depending on the environtment. - - Keyword Arguments: - environ {dictionary} -- WSGI environ dictionary. (default: {None}) - """ - self.cookie_jar = CookieJar() - self.header_bag = HeaderBag() - self.url_params = {} - self.redirect_url = False - self.redirect_route = False - self.user_model = None - self.subdomain = None - self._activate_subdomains = False - self.request_variables = {} - self._test_user = False - self.raw_input = None - self.query_params = {} - - if environ: - self.load_environ(environ) - - self.encryption_key = False - self.container = None - - def input(self, name, default=False, clean=False, quote=True): - """Get a specific input value. - - Arguments: - name {string} -- Key of the input data - - Keyword Arguments: - default {string} -- Default value if input does not exist (default: {False}) - clean {bool} -- Whether or not the return value should be - cleaned (default: {True}) - - Returns: - string - """ - name = str(name) - - if "." in name and isinstance( - self.request_variables.get(name.split(".")[0]), dict - ): - return clean_request_input( - DictDot().dot(name, self.request_variables, default=default), - clean=clean, - ) - - elif "." in name: - name = dot(name, "{1}[{.}]") - - return clean_request_input( - self.request_variables.get(name, default), clean=clean, quote=quote - ) - - def query(self, name, default=None, multi=False): - """Get a specific query string value. - - Arguments: - name {string} -- Key of the input data - - Keyword Arguments: - default {any} -- Default value if input does not exist (default: {None}) - multi {bool} -- Whether to return all values of a multi value query string param - ie. quuery_param=one&query_param=two (default: {False}) - - Returns: - any - """ - try: - value = self.query_params[name] - except KeyError: - return default - - if not multi and value: - return value[0] - return value - - def all_query(self): - """Get all query string values - - Returns: - any - """ - return self.query_params - - def is_post(self): - """Check if the current request is a POST request. - - Returns: - bool - """ - if self.environ["REQUEST_METHOD"] == "POST": - return True - - return False - - def is_not_get_request(self): - """Check if the current request is not a get request. - - Returns: - bool - """ - if not self.environ["REQUEST_METHOD"] == "GET": - return True - - return False - - def is_not_safe(self): - """Check if the current request is not a get request. - - Returns: - bool - """ - if ( - not self.environ["REQUEST_METHOD"] == "GET" - and not self.environ["REQUEST_METHOD"] == "OPTIONS" - and not self.environ["REQUEST_METHOD"] == "HEAD" - ): - return True - - return False - - def __set_request_method(self): - """Private method for manually setting the request method. - - Returns: - bool - """ - if self.has("__method"): - self.environ["REQUEST_METHOD"] = self.input("__method") - return True - - return False - - def key(self, key): - """Set the encryption key. - - Arguments: - key {string} -- Encryption key - - Returns: - self - """ - self.encryption_key = key - return self - - def all(self, internal_variables=True, clean=True, quote=True): - """Get all the input data. - - Keyword Arguments: - internal_variables {bool} -- Get the internal framework variables - as well (default: {True}) - clean {bool} -- Whether or not the return value should be - cleaned (default: {True}) - - Returns: - dict - """ - - if isinstance(self.raw_input, list): - return self.raw_input - - if not internal_variables: - without_internals = {} - for key, value in self.request_variables.items(): - if not key.startswith("__"): - without_internals.update({key: value}) - return clean_request_input(without_internals, clean=clean, quote=quote) - - return clean_request_input(self.request_variables, clean=clean, quote=quote) - - def only(self, *names): - """Return the specified request variables in a dictionary. - - Returns: - dict - """ - only_vars = {} - - for name in names: - only_vars[name] = self.request_variables.get(name) - - return only_vars - - def without(self, *names): - """Return the request variables in a dictionary without specified values. - - Returns: - dict - """ - only_vars = {} - - for name in self.request_variables: - if name not in names: - only_vars[name] = self.request_variables.get(name) - - return only_vars - - def load_app(self, app): - """Load the container into the request class. - - Arguments: - app {masonite.app.App} -- Application Container - - Returns: - self - """ - self.container = app - return self - - def load_environ(self, environ): - """Load the wsgi environment and sets various properties. - - Arguments: - environ {dict} -- WSGI environ - - Returns: - self - """ - self.environ = environ - self.header_bag.load(environ) - self.method = environ["REQUEST_METHOD"] - self.path = environ["PATH_INFO"] - self.request_variables = {} - self.raw_input = None - - if "QUERY_STRING" in environ and environ["QUERY_STRING"]: - self.query_params = parse_qs(environ["QUERY_STRING"]) - - if self.is_not_get_request(): - environ["POST_DATA"] = self.get_post_params() - - if "POST_DATA" in environ: - self._set_standardized_request_variables(environ["POST_DATA"]) - elif "QUERY_STRING" in environ and environ["QUERY_STRING"]: - self._set_standardized_request_variables(environ["QUERY_STRING"]) - - if "HTTP_COOKIE" in environ: - self.cookie_jar.load(environ["HTTP_COOKIE"]) - - if self.has("__method"): - self.__set_request_method() - - return self - - def get_post_params(self): - """Return the correct input. - - Returns: - dict -- Dictionary of post parameters. - """ - fields = None - if ( - "CONTENT_TYPE" in self.environ - and "application/json" in self.environ["CONTENT_TYPE"].lower() - ): - try: - request_body_size = int(self.environ.get("CONTENT_LENGTH", 0)) - except ValueError: - request_body_size = 0 - - request_body = self.environ["wsgi.input"].read(request_body_size) - - if isinstance(request_body, bytes): - request_body = request_body.decode("utf-8") - - return json.loads(request_body or "{}") - else: - fields = cgi.FieldStorage( - fp=self.environ["wsgi.input"], - environ=self.environ, - keep_blank_values=1, - ) - return fields - - def _set_standardized_request_variables(self, variables): - """The input data is not perfect so we have to standardize it into a dictionary. - - Arguments: - variables {string|dict} - """ - # vv = variables - if isinstance(variables, str): - variables = query_parse(variables) - - self.raw_input = variables - if isinstance(variables, list): - variables = {str(i): v for i, v in enumerate(variables)} - - try: - for name in variables.keys(): - value = self._get_standardized_value(variables[name]) - self.request_variables[name.replace("[]", "")] = value - return - except TypeError: - pass - - self.request_variables = {} - - def _get_standardized_value(self, value): - """Get the standardized value based on the type of the value parameter. - - Arguments: - value {list|dict|cgi.FileStorage|string} - - Returns: - string|bool - """ - if value is None: - return None - - if isinstance(value, list): - - # If the list contains MiniFieldStorage objects then loop - # through and get the values. - if any(isinstance(storage_obj, MiniFieldStorage) for storage_obj in value): - values = [storage_obj.value for storage_obj in value] - - # TODO: This needs to be removed in 2.2. A breaking change but - # this code will result in inconsistent values - # If there is only 1 element in the list then return the only value in the list - if len(values) == 1: - return values[0] - return values - - return value - - if isinstance(value, (str, int, dict)): - return value - - if not value.filename: - return value.value - - if value.filename: - return value - - return False - - def app(self): - """Return the application container. - - Returns: - masonite.app.App -- Application container - """ - # if self.container is None: - # raise AttributeError("The container has not been loaded into the Request class. Use the 'load_app' method to load the container.") - return self.container - - def has(self, *args): - """Check if all given keys in request variable exists. - - Returns: - bool - """ - return all((arg in self.request_variables) for arg in args) - - def scheme(self): - """Get the current request url scheme - - Returns: - string -- the scheme used for the request (http|https) - """ - return self.environ["wsgi.url_scheme"] - - def referrer(self): - """Gets the URL of the request that the current URL came from. - - Returns: - string -- Returns the previous referring URL. - """ - - return self.environ.get("HTTP_REFERER") - - def host(self): - """Get the server's hostname for the current request. - - Returns: - string -- the hostname - """ - host = self.environ.get("HTTP_HOST") - if not host: - host = self.environ["SERVER_NAME"] - return host.split(":", 1)[0] - - def port(self): - """Get the server's port number for the current request. - - Returns: - string -- the server's port number. - """ - return self.environ["SERVER_PORT"] - - def full_path(self, quoted=True): - """Get the path part of the current request url. (including the application path). - - Args: - quoted {bool} -- whether to escape special chars (default: {True}). - - Returns: - string -- the path of the url - """ - url = self.environ.get("SCRIPT_NAME", "") + self.environ.get("PATH_INFO", "") - if quoted: - url = quote(url) - return url - - def url(self, include_standard_port=False): - """Get the url of the current request including the scheme://host:port/path. - - Args: - include_standard_port {bool} -- whether to include the port - when the request uses the standard http(s) port (default: {False}). - - Returns: - string -- the requested url. - """ - scheme = self.scheme() - host = self.host() - port = self.port() - path = self.full_path() - if ( - include_standard_port - or (scheme == "https" and port != "443") - or (scheme == "http" and port != "80") - ): - port_part = ":{}".format(port) - else: - port_part = "" - return "{}://{}{}{}".format(scheme, host, port_part, path) - - def full_url(self, include_standard_port=False): - """Get the full url including query string of the current request. - example: - scheme://host:port/path?query-string - - Args: - include_standard_port {bool} -- whether to include the port - when the request uses the standard http(s) port (default: {False}). - - Returns: - string -- The full request url - """ - url = self.url(include_standard_port=include_standard_port) - query_string = self.query_string() - if query_string: - return "{}?{}".format(url, query_string) - else: - return url - - def query_string(self): - """Get the raw query string of the current request url. - - Returns: - string -- The query-string of the request - """ - return self.environ.get("QUERY_STRING", "") - - def status(self, status): - """Set the HTTP status code. - - Arguments: - status {string|integer} -- A string or integer with the standardized status code - - Returns: - self - """ - return self.app().make(Response).status(status) - - def route_exists(self, url): - web_routes = self.container.make("WebRoutes") - - for route in web_routes: - if route.route_url == url: - return True - - return False - - def get_request_method(self): - """Get the current request method. - - Returns: - string -- returns GET, POST, PUT, etc - """ - return self.environ["REQUEST_METHOD"] - - def header(self, key, value=None): - """Set or gets a header depending on if "value" is passed in or not. - - Arguments: - key {string|dict} -- The header you want to set or get. If the key is a dictionary, loop through each key pair - and add them to the headers. - - Keyword Arguments: - value {string} -- The value you want to set (default: {None}) - - Returns: - string|None|True -- Either return the value if getting a header, - None if it doesn't exist or True if setting the headers. - """ - if isinstance(key, dict): - for dic_key, dic_value in key.items(): - self._set_header(dic_key, dic_value) - return - - # Get Headers - if value is None: - header = self.header_bag.get(key) - if header: - return header.value - return "" - - self._set_header(key, value) - - def _set_header(self, key, value): - # Set Headers - - self.header_bag.add(Header(key, value)) - - def has_raw_header(self, key): - return key in self.header_bag - - def get_headers(self): - """Return all current headers to be set. - - Returns: - list -- List containing a tuple of headers. - """ - - return self._compile_headers_to_tuple() + self.cookie_jar.render_response() - - def _compile_headers_to_tuple(self): - """Compiles the current headers to a list of tuples. - - Returns: - list -- A list of tuples. - """ - - return self.header_bag.render() - - def reset_headers(self): - """Reset all headers being set. - - Typically ran at the end of the request - because of this object acts like a singleton. - - Returns: - None - """ - self.header_bag = HeaderBag() - - def get_and_reset_headers(self): - """Gets the headers but resets at the same time. - - This is useful at the end of the WSGI request to prevent - Several requests from - - Returns: - tuple - """ - headers = self.get_headers() - self.reset_headers() - self.url_params = {} - self.cookie_jar = CookieJar() - return headers - - def set_params(self, params): - """Load the params into the class. - - These parameters are where the developer can retrieve the - /url/@variable:string/ from the url. - - Arguments: - params {dict} -- Dictionary of parameters to store on the class. - - Returns: - self - """ - self.url_params = params - return self - - def param(self, parameter): - """Retrieve the param from the URL. - - The "parameter" parameter in this method should be the name of the - @variable passed into the url in web.py. - - Arguments: - parameter {string} -- Specific argument to return. - - Returns: - string|False -- Returns False if key does not exist. - """ - if parameter in self.url_params: - return self.url_params.get(parameter) - return False - - def cookie( - self, - key, - value, - encrypt=True, - http_only="HttpOnly;", - path="/", - expires=None, - secure=False, - ): - """Set a cookie in the browser. - - Arguments: - key {string} -- Name of the cookie you want set. - value {string} -- Value of the cookie you want set. - - Keyword Arguments: - encrypt {bool} -- Whether or not you want to encrypt the - cookie (default: {True}) - http_only {str} -- If the cookie is HttpOnly or not (default: {"HttpOnly;"}) - path {str} -- The path of the cookie to be set to. (default: {'/'}) - expires {string} -- When the cookie expires - (5 minutes, 1 minute, 10 hours, etc) (default: {''}) - - Returns: - self - """ - - if self.environ.get("SECURE_COOKIES") == "True": - secure = True - - if encrypt: - value = Sign(self.encryption_key).sign(value) - else: - value = value - - if expires: - expires = cookie_expire_time(expires) - - self.cookie_jar.add( - key, - value, - expires=expires, - http_only=http_only, - secure=secure, - path=path, - timezone="GMT", - ) - - return self - - def get_cookies(self): - """Retrieve all cookies from the browser. - - Returns: - dict -- Returns all the cookies. - """ - return self.cookie_jar - - def get_raw_cookie(self, provided_cookie): - return self.cookie_jar.get(provided_cookie) - - def get_cookie(self, provided_cookie, decrypt=True): - """Retrieve a specific cookie from the browser. - - Arguments: - provided_cookie {string} -- Name of the cookie to retrieve - - Keyword Arguments: - decrypt {bool} -- Whether Masonite should try to decrypt the cookie. - This should only be True if the cookie was encrypted - in the first place. (default: {True}) - - Returns: - string|None -- Returns None if the cookie does not exist. - """ - if decrypt: - try: - return Sign(self.encryption_key).unsign( - self.cookie_jar.get(provided_cookie).value - ) - except InvalidToken: - self.delete_cookie(provided_cookie) - return None - except AttributeError: - pass - if self.cookie_jar.exists(provided_cookie): - return self.cookie_jar.get(provided_cookie).value - - def append_cookie(self, value): - """Append cookie to the string or create a new string. - - Whether a new cookie should append on to the string of cookies to be set - or create a new string. This string is used by the browser to interpret how - handle setting a cookie. - - Arguments: - key {string} -- Name of cookie to be stored - value {string} -- Value of cookie to be stored - """ - if "HTTP_COOKIE" in self.environ and self.environ["HTTP_COOKIE"]: - self.environ["HTTP_COOKIE"] += ";{}".format(value) - else: - self.environ["HTTP_COOKIE"] = "{}".format(value) - - def delete_cookie(self, key): - """Delete cookie. - - Arguments: - key {string} -- Name of cookie to be deleted. - - Returns: - bool -- Whether or not the cookie was successfully deleted. - """ - self.cookie_jar.delete(key) - - self.cookie(key, "", expires="expired") - - def set_user(self, user_model): - """Load the user into the class. - - Arguments: - user_model {app.User.User} -- Defaults to loading this class - unless specifically changed. - - Returns: - self - """ - if self._test_user: - self.user_model = self._test_user - else: - self.user_model = user_model - - return self - - def reset_user(self): - """Resets the user back to none""" - self.user_model = None - - def user(self): - """Load the user into the class. - - Returns: - app.User.User|None -- Returns None if the user is not loaded or logged in. - """ - # if self.app().has("User") and self.app().make("User"): - # return self.app().make("User") - return self.user_model - - def redirect( - self, route=None, params={}, name=None, controller=None, url=None, status=302 - ): - """Redirect the user based on the route specified. - - Arguments: - route {string} -- URI of the route (/dashboard/user) - - Keyword Arguments: - params {dict} -- Dictionary of parameters to set for the URI. - Use this when the URI has something like - /dashboard/user/@id. (default: {{}}) - - Returns: - self - """ - if name: - return self.redirect_to(name, params, status=status) - elif route: - self.redirect_url = self.compile_route_to_url(route, params) - elif controller: - self.redirect_url = self.url_from_controller(controller, params) - elif url: - self.redirect_url = url - - self.status(status) - return self - - def with_input(self): - self.flash_inputs_to_session() - return self - - def redirect_to(self, route_name, params={}, status=302): - """Redirect to a named route. - - Arguments: - route_name {string} -- Name of a named route. - - Keyword Arguments: - params {dict} -- Dictionary of parameters to set for the URI. - Use this when the URI has something like - /dashboard/user/@id. (default: {{}}) - - Returns: - self - """ - self.redirect_url = self._get_named_route(route_name, params) - self.status(status) - - return self - - def _get_named_route(self, name, params): - """Search the list of routes and returns the route with the name passed. - - Arguments: - name {string} -- Route name to search for (dashboard.user). - params {dict} -- Dictionary of items to pass to the named route. - - Returns: - string|None -- Returns None if the route was not found or returns the - compiled URI. - """ - web_routes = self.container.make("WebRoutes") - - for route in web_routes: - if route.named_route == name: - return self.compile_route_to_url(route.route_url, params) - - raise RouteException( - "Could not find the route with the name of '{}'".format(name) - ) - - def _get_route_from_controller(self, controller): - """Get the route using the controller. - - This finds the route with the attached controller and returns that route. - This does not compile the URI but actually returns the Route object. - - Arguments: - controller {string|object} -- Can pass in either a string controller - or the controller itself (the object) - - Returns: - masonite.routes.Route|None -- Returns None if the route could not be found. - """ - web_routes = self.container.make("WebRoutes") - - if not isinstance(controller, str): - module_location = controller.__module__ - controller = controller.__qualname__.split(".") - else: - module_location = "app.http.controllers" - controller = controller.split("@") - - for route in web_routes: - if ( - route.controller.__name__ == controller[0] - and route.controller_method == controller[1] - and route.module_location == module_location - ): - return route - - def url_from_controller(self, controller, params={}): - """Return the compiled URI using a controller. - - Arguments: - controller {string|object} -- Can be a string controller or - a controller object. - - Keyword Arguments: - params {dict} -- Dictionary of parameters to pass to the route - for compilation. (default: {{}}) - - Returns: - masonite.routes.Route|None -- Returns None if the route could not be found. - """ - return self.compile_route_to_url( - self._get_route_from_controller(controller).route_url, params - ) - - def route(self, name, params={}, full=False): - """Get a route URI by its name. - - Arguments: - name {string} -- Name of the route. - - Keyword Arguments: - params {dict} -- Dictionary of parameters to pass to the route - for compilation. (default: {{}}) - full {bool} -- Specifies whether the full application url should - be returned or not. (default: {False}) - - Returns: - masonite.routes.Route|None -- Returns None if the route cannot be found. - """ - from config import application - - if full: - route = application.URL + self._get_named_route(name, params) - else: - try: - route = self._get_named_route(name, params) - except KeyError: - params = {} - params.update(self.url_params) - route = self._get_named_route(name, params) - - if not route: - raise RouteException( - "Route with the name of '{}' was not found.".format(name) - ) - - return route - - def __getattr__(self, key): - inp = self.input(key) - if inp: - return inp - - inp = self.param(key) - if inp: - return inp - - raise AttributeError("class 'Request' has no attribute {}".format(key)) - - def with_errors(self, errors): - """Easily attach errors message to session request.""" - return self.with_flash("errors", errors) - - def with_success(self, success): - """Easily attach success message to session request.""" - return self.with_flash("success", success) - - def with_flash(self, key, value): - """Easily attach data to session request.""" - self.session.flash(key, value) - return self - - def reset_redirections(self): - """Reset the redirections because of this class acting like a singleton pattern.""" - self.redirect_url = False - self.redirect_route = False - - def back(self, default=None): - """Return a URI for redirection depending on several use cases. - - Keyword Arguments: - default {string} -- Default value if nothing can be found. (default: {None}) - - Returns: - self - """ - self.with_input() - - redirect_url = self.input("__back") - - if not redirect_url and default: - return self.redirect(url=default) - elif not redirect_url and not default: - return self.redirect(url=self.path) - - return self.redirect(url=redirect_url) - - def then_back(self): - self.session.set("__intend", self.path) - return self - - def redirect_intended(self, default=None): - if self.session.get("__intend"): - self.redirect(self.session.get("__intend")) - self.session.delete("__intend") - else: - self.redirect(default) - - return self - - def flash_inputs_to_session(self): - if not hasattr(self, "session"): - return - - for key, value in self.all().items(): - if isinstance(value, bytes): - continue - - self.session.flash(key, value) - - def is_named_route(self, name, params={}): - """Check if the current URI is a specific named route. - - Arguments: - name {string} -- The name of a route. - - Keyword Arguments: - params {dict} -- Dictionary of parameters to pass to the route. (default: {{}}) - - Returns: - bool - """ - if self._get_named_route(name, params) == self.path: - return True - - return False - - def contains(self, route, show=None): - """If the specified URI is in the current URI path. - - Arguments: - route {string} -- Part of a URI (/dashboard) - - Returns: - bool - """ - if show is not None: - if re.match(compile_route_to_regex(route), self.path): - return show - - return "" - - return re.match(compile_route_to_regex(route), self.path) - - def compile_route_to_url(self, route, params={}): - """Compile the route url into a usable url. - - Converts /url/@id into /url/1. Used for redirection - - Arguments: - route {string} -- An uncompiled route - like (/dashboard/@user:string/@id:int) - - Keyword Arguments: - params {dict} -- Dictionary of parameters to pass to the route (default: {{}}) - - Returns: - string -- Returns a compiled string (/dashboard/joseph/1) - """ - if "http" in route: - return route - - # Split the url into a list - split_url = route.split("/") - - # Start beginning of the new compiled url - compiled_url = "/" - - # Iterate over the list - for url in split_url: - if url: - # if the url contains a parameter variable like @id:int - if "@" in url: - url = url.replace("@", "").split(":")[0] - if isinstance(params, dict): - compiled_url += str(params[url]) + "/" - elif isinstance(params, list): - compiled_url += str(params.pop(0)) + "/" - elif "?" in url: - url = url.replace("?", "").split(":")[0] - if isinstance(params, dict): - compiled_url += str(params.get(url, "/")) + "/" - elif isinstance(params, list): - compiled_url += str(params.pop(0)) + "/" - else: - compiled_url += url + "/" - - compiled_url = compiled_url.replace("//", "") - # The loop isn't perfect and may have an unwanted trailing slash - if compiled_url.endswith("/") and not route.endswith("/"): - compiled_url = compiled_url[:-1] - - # The loop isn't perfect and may have 2 slashes next to eachother - if "//" in compiled_url: - compiled_url = compiled_url.replace("//", "/") - - return compiled_url - - def activate_subdomains(self): - """Activate subdomains abilities.""" - self.app().bind("Subdomains", True) - - def has_subdomain(self): - """Check if the current URI has a subdomain. - - Returns: - bool - """ - if self.app().has("Subdomains") and self.app().make("Subdomains"): - url = tldextract.extract(self.environ["HTTP_HOST"]) - - if url.subdomain: - self.subdomain = url.subdomain - self.url_params.update({"subdomain": self.subdomain}) - return True - - return False - - def send(self, params): - """DEPRECATED :: sets a dictionary to be compiled for a route. - - Arguments: - params {dict} -- Dictionary of parameters you want to pass to the route. - - Returns: - self - """ - self.set_params(params) - return self - - def helper(self): - """Dummy method to work with returning the class. Used for helper methods in the View class. - - Returns: - self - """ - return self - - def pop(self, *input_variables): - """Delete keys from the request input.""" - for key in input_variables: - if key in self.request_variables: - del self.request_variables[key] - - def validate(self, *rules): - validator = self.app().make("Validator") - return validator.validate(self.request_variables, *rules) diff --git a/src/masonite/request/__init__.py b/src/masonite/request/__init__.py new file mode 100644 index 000000000..c8b376b74 --- /dev/null +++ b/src/masonite/request/__init__.py @@ -0,0 +1 @@ +from .request import Request diff --git a/src/masonite/request/request.py b/src/masonite/request/request.py new file mode 100644 index 000000000..01cd635d7 --- /dev/null +++ b/src/masonite/request/request.py @@ -0,0 +1,137 @@ +from ..cookies import CookieJar +from ..headers import HeaderBag, Header +from ..input import InputBag +import re +import tldextract +from .validation import ValidatesRequest +from ..authorization import AuthorizesRequest + + +class Request(ValidatesRequest, AuthorizesRequest): + def __init__(self, environ): + """Request class constructor. + + Initializes several properties and sets various methods + depending on the environtment. + + Keyword Arguments: + environ {dictionary} -- WSGI environ dictionary. (default: {None}) + """ + self.environ = environ + self.cookie_jar = CookieJar() + self.header_bag = HeaderBag() + self.input_bag = InputBag() + self.params = {} + self._user = None + self.load() + + def load(self): + self.cookie_jar.load(self.environ.get("HTTP_COOKIE", "")) + self.header_bag.load(self.environ) + self.input_bag.load(self.environ) + + def load_params(self, params=None): + if not params: + params = {} + + self.params.update(params) + + def param(self, param, default=""): + return self.params.get(param, default) + + def get_path(self): + return self.environ.get("PATH_INFO") + + def get_back_path(self): + return self.input("__back") or self.get_path() + + def get_request_method(self): + return self.environ.get("REQUEST_METHOD") + + def input(self, name, default=False): + """Get a specific input value. + + Arguments: + name {string} -- Key of the input data + + Keyword Arguments: + default {string} -- Default value if input does not exist (default: {False}) + clean {bool} -- Whether or not the return value should be + cleaned (default: {True}) + + Returns: + string + """ + name = str(name) + + return self.input_bag.get(name, default=default) + + def cookie(self, name, value=None, **options): + if value is None: + cookie = self.cookie_jar.get(name) + if not cookie: + return + return cookie.value + + return self.cookie_jar.add(name, value, **options) + + def delete_cookie(self, name): + self.cookie_jar.delete(name) + return self + + def header(self, name, value=None): + if value is None: + header = self.header_bag.get(name) + if not header: + return + return header.value + else: + return self.header_bag.add(Header(name, value)) + + def all(self): + return self.input_bag.all_as_values() + + def only(self, *inputs): + return self.input_bag.only(*inputs) + + def is_not_safe(self): + """Check if the current request is not a get request. + + Returns: + bool + """ + if not self.get_request_method() in ("GET", "OPTIONS", "HEAD"): + return True + + return False + + def user(self): + return self._user + + def set_user(self, user): + self._user = user + return self + + def remove_user(self): + self._user = None + return self + + def contains(self, route): + if not route.startswith("/"): + route = "/" + route + + regex = re.compile(route.replace("*", "[a-zA-Z0-9_]+")) + + return regex.match(self.get_path()) + + def get_subdomain(self, exclude_www=True): + url = tldextract.extract(self.get_host()) + if url.subdomain == "" or ( + url.subdomain and exclude_www and url.subdomain == "www" + ): + return None + + return url.subdomain + + def get_host(self): + return self.environ.get("HTTP_HOST") diff --git a/src/masonite/request/validation.py b/src/masonite/request/validation.py new file mode 100644 index 000000000..ba276cdb1 --- /dev/null +++ b/src/masonite/request/validation.py @@ -0,0 +1,7 @@ +from ..validation import Validator + + +class ValidatesRequest: + def validate(self, *rules): + validator = Validator() + return validator.validate(self.all(), *rules) diff --git a/src/masonite/response/__init__.py b/src/masonite/response/__init__.py new file mode 100644 index 000000000..b2c18bf4a --- /dev/null +++ b/src/masonite/response/__init__.py @@ -0,0 +1 @@ +from .response import Response diff --git a/src/masonite/response.py b/src/masonite/response/response.py similarity index 65% rename from src/masonite/response.py rename to src/masonite/response/response.py index bbdffa68f..81455c34b 100644 --- a/src/masonite/response.py +++ b/src/masonite/response/response.py @@ -4,28 +4,28 @@ import mimetypes from pathlib import Path -from .app import App -from .exceptions import ResponseError -from .helpers.Extendable import Extendable -from .headers import HeaderBag, Header -from .helpers.status import response_statuses -from .exceptions import InvalidHTTPStatusCode +from ..routes.Router import Router +from ..exceptions import ResponseError, InvalidHTTPStatusCode +from ..headers import HeaderBag, Header +from ..utils.http import HTTP_STATUS_CODES +from ..cookies import CookieJar -class Response(Extendable): +class Response: """A Response object to be used to abstract the logic of getting a response ready to be returned. Arguments: app {masonite.app.App} -- The Masonite container. """ - def __init__(self, app: App): + def __init__(self, app): self.app = app - self.request = self.app.make("Request") self.content = "" self._status = None - self.statuses = response_statuses() + self.statuses = HTTP_STATUS_CODES self.header_bag = HeaderBag() + self.cookie_jar = CookieJar() + self.original = None def json(self, payload, status=200): """Gets the response ready for a JSON response. @@ -56,17 +56,30 @@ def make_headers(self, content_type="text/html; charset=utf-8"): def header(self, name, value=None): if value is None and isinstance(name, dict): for name, value in name.items(): - self.header_bag.add(Header(name, value)) + self.header_bag.add(Header(name, str(value))) elif value is None: - return self.header_bag.get(name) + header = self.header_bag.get(name) + if isinstance(header, str): + return header + return header.value return self.header_bag.add(Header(name, value)) - def get_and_reset_headers(self): - header = self.header_bag - self.header_bag = HeaderBag() - self._status = None - return header.render() + self.request.cookie_jar.render_response() + def get_headers(self): + return self.header_bag.render() + + def cookie(self, name, value=None, **options): + if value is None: + cookie = self.cookie_jar.get(name) + if not cookie: + return + return cookie.value + + return self.cookie_jar.add(name, value, **options) + + def delete_cookie(self, name): + self.cookie_jar.delete(name) + return self def get_response_content(self): return self.data() @@ -144,7 +157,7 @@ def view(self, view, status=200): Returns: string|dict|list -- Returns the data to be returned. """ - + self.original = view if isinstance(view, tuple): view, status = view self.status(status) @@ -158,10 +171,8 @@ def view(self, view, status=200): return self.json(view.serialize(), status=self.get_status_code()) elif isinstance(view, int): view = str(view) - elif isinstance(view, Responsable) or hasattr(view, "get_response"): + elif hasattr(view, "get_response"): view = view.get_response() - elif isinstance(view, self.request.__class__): - view = self.data() elif view is None: raise ResponseError( "Responses cannot be of type: None. Did you return anything in your responsable method?" @@ -169,34 +180,44 @@ def view(self, view, status=200): if isinstance(view, str): self.content = bytes(view, "utf-8") - self.app.bind("Response", bytes(view, "utf-8")) else: self.content = view - self.app.bind("Response", view) self.make_headers() - return self.data() + return self - def redirect(self, location=None, status=302): + def back(self): + return self.redirect(url=self.app.make("request").get_back_path()) + + def redirect(self, location=None, name=None, params={}, url=None, status=302): """Set the redirection on the server. Keyword Arguments: location {string} -- The URL to redirect to (default: {None}) status {int} -- The Response status code. (default: {302}) + params {dict} -- The route params (default: {}) Returns: string -- Returns the data to be returned. """ self.status(status) - if not location: - location = self.request.redirect_url - self.request.reset_headers() - self.header_bag.add(Header("Location", location)) + if location: + self.header_bag.add(Header("Location", location)) + elif name: + url = self._get_url_from_route_name(name, params) + self.header_bag.add(Header("Location", url)) + elif url: + self.header_bag.add(Header("Location", url)) self.view("Redirecting ...") + return self - return self.data() + def _get_url_from_route_name(self, name, params={}): + route = self.app.make("router").find_by_name(name) + if not route: + raise ValueError(f"Route with the name '{name}' not found.") + return Router.compile_to_url(route.url, params) def to_bytes(self): """Converts the data to bytes so the WSGI server can handle it. @@ -206,69 +227,23 @@ def to_bytes(self): """ return self.converted_data() - -class Responsable: - def get_response(self): - raise NotImplementedError( - "This class does not implement a 'get_response()' method" - ) - - -class Download(Responsable): - """Download class to help show files in the browser or force - a download for the client browser. - - Arguments: - location {string} -- The path you want to download. - - Keyword Arguments: - force {bool} -- Whether you want the client's browser to force the file download (default: {False}) - name {str} -- The name you want the file to be called when downloaded (default: {'profile.jpg'}) - """ - - def __init__(self, location, force=False, name="1"): - self.location = location - self._force = force - self.name = name - self.container = None - - def force(self): - """Sets the force option. - - Returns: - self - """ - self._force = True - return self - - def get_response(self): - """Handles the way the response should be handled by the server. - - Returns: - bytes - Returns bytes required for the server to handle the download. - """ - if not self.container: - from wsgi import container - - self.container = container - - response = self.container.make(Response) - - with open(self.location, "rb") as filelike: - data = filelike.read() - - if self._force: - response.header("Content-Type", "application/octet-stream") - response.header( + def download(self, name, location, force=False): + if force: + self.header("Content-Type", "application/octet-stream") + self.header( "Content-Disposition", - 'attachment; filename="{}{}"'.format( - self.name, self.extension(self.location) - ), + 'attachment; filename="{}{}"'.format(name, self.extension(location)), ) else: - response.header("Content-Type", self.mimetype(self.location)) + self.header("Content-Type", self.mimetype(location)) + + with open(location, "rb") as filelike: + data = filelike.read() + + return self.view(data) - return data + def extension(self, path): + return Path(path).suffix def mimetype(self, path): """Gets the mimetime of a path @@ -280,6 +255,3 @@ def mimetype(self, path): string -- The mimetype for use in headers """ return mimetypes.guess_type(path)[0] - - def extension(self, path): - return Path(path).suffix diff --git a/src/masonite/routes.py b/src/masonite/routes.py deleted file mode 100644 index 777946cff..000000000 --- a/src/masonite/routes.py +++ /dev/null @@ -1,772 +0,0 @@ -"""Module for the Routing System.""" - -import cgi -import importlib -import json -import re - -from .exceptions import ( - RouteMiddlewareNotFound, - InvalidRouteCompileException, - RouteException, -) -from .view import View - - -class Route: - """Route class used to handle routing.""" - - route_compilers = { - "int": r"(\d+)", - "integer": r"(\d+)", - "string": r"([a-zA-Z]+)", - "default": r"([\w.-]+)", - "signed": r"([\w\-=]+)", - } - - def __init__(self, environ=None): - """Route constructor. - - Keyword Arguments: - environ {dict} -- WSGI environ (default: {None}) - """ - self.url_list = [] - self.method_type = ["GET"] - - if environ: - self.load_environ(environ) - - def load_environ(self, environ): - """Load the WSGI environ into the class. - - Arguments: - environ {dict} -- WSGI environ - - Returns: - self - """ - self.environ = environ - self.url = environ["PATH_INFO"] - - return self - - def is_post(self): - """Check to see if the current request is a POST request. - - Returns: - bool - """ - if self.environ["REQUEST_METHOD"] == "POST": - return True - - return False - - def is_not_get_request(self): - """Check if current request is not a get request. - - Returns: - bool - """ - if not self.environ["REQUEST_METHOD"] == "GET": - return True - - return False - - def compile(self, key, to=""): - self.route_compilers.update({key: to}) - return self - - def generated_url_list(self): - """Return the URL list. - - Returns: - list -- URL list. - """ - return self.url_list - - -class BaseHttpRoute: - """Base route for HTTP routes.""" - - def __init__(self): - self.method_type = ["GET"] - self.output = False - self.route_url = None - self.request = None - self.named_route = None - self.required_domain = None - self.module_location = "app.http.controllers" - self.list_middleware = [] - self.default_parameters = {} - self.e = False - - def default(self, dictionary): - self.default_parameters.update(dictionary) - return self - - def get_default_parameter(self, key): - return self.default_parameters.get(key, None) - - def route(self, route, output): - """Load the route into the class. This also looks for the controller and attaches it to the route. - - Arguments: - route {string} -- This is a URI to attach to the route (/dashboard/user). - output {string|object} -- Controller to attach to the route. - - Returns: - self - """ - self.output = output - self._find_controller(output) - - if not route.startswith("/"): - route = "/" + route - - if route.endswith("/") and route != "/": - route = route[:-1] - - self.route_url = route - self._compiled_url = self.compile_route_to_regex() - return self - - def view(self, route, template, dictionary={}): - view_route = ViewRoute(self.method_type, route, template, dictionary) - return view_route - - def _find_controller(self, controller): - """Find the controller to attach to the route. - - Arguments: - controller {string|object} -- String or object controller to search for. - - Returns: - None - """ - module_location = self.module_location - # If the output specified is a string controller - if isinstance(controller, str): - mod = controller.split("@") - # If trying to get an absolute path via a string - if mod[0].startswith("/"): - module_location = ".".join(mod[0].replace("/", "").split(".")[0:-1]) - elif "." in mod[0]: - # This is a deeper module controller - module_location += "." + ".".join(mod[0].split(".")[:-1]) - else: - if controller is None: - return None - - fully_qualified_name = controller.__qualname__ - mod = fully_qualified_name.split(".") - module_location = controller.__module__ - - # Gets the controller name from the output parameter - # This is used to add support for additional modules - # like 'LoginController' and 'Auth.LoginController' - get_controller = mod[0].split(".")[-1] - - try: - # Import the module - if isinstance(controller, str): - module = importlib.import_module( - "{0}.".format(module_location) + get_controller - ) - else: - module = importlib.import_module("{0}".format(module_location)) - - # Get the controller from the module - self.controller = getattr(module, get_controller) - - # Set the controller method on class. This is a string - self.controller_method = mod[1] if len(mod) == 2 else "__call__" - except ImportError as e: - import sys - import traceback - - _, _, exc_tb = sys.exc_info() - self.e = e - except Exception as e: # skipcq - import sys - import traceback - - _, _, exc_tb = sys.exc_info() - self.e = e - print("\033[93mTrouble importing controller!", str(e), "\033[0m") - if not self.e: - self.module_location = module_location - - def get_response(self): - # Resolve Controller Constructor - if self.e: - print( - "\033[93mCannot find controller {}. Did you create this one?".format( - self.output - ), - "\033[0m", - ) - raise SyntaxError(str(self.e)) - - controller = self.request.app().resolve(self.controller) - - # Resolve Controller Method - response = self.request.app().resolve( - getattr(controller, self.controller_method), - *self.request.url_params.values() - ) - # save original content - self.original = response - - if isinstance(response, View): - response = response.rendered_template - - return response - - def domain(self, domain): - """Set the subdomain for the route. - - Arguments: - domain {string|list|tuple} -- The string or list of subdomains to attach to this route. - - Returns: - self - """ - self.required_domain = domain - return self - - def module(self, module): - """DEPRECATED :: The base module to look for string controllers. - - Arguments: - module {string} -- The string representation of a module to look for controllers. - - Returns: - self - """ - self.module_location = module - return self - - def has_required_domain(self): - """Check if the route has the required subdomain before executing the route. - - Returns: - bool - """ - if self.request.has_subdomain() and ( - self.required_domain == "*" - or self.request.subdomain == self.required_domain - ): - return True - return False - - def name(self, name): - """Specify the name of the route. - - Arguments: - name {string} -- Sets a name for the route. - - Returns: - self - """ - self.named_route = name - return self - - def load_request(self, request): - """Load the request into this class. - - Arguments: - request {masonite.request.Request} -- Request object. - - Returns: - self - """ - self.request = request - return self - - def middleware(self, *args): - """Load a list of middleware to run. - - Returns: - self - """ - for arg in args: - if arg not in self.list_middleware: - self.list_middleware.append(arg) - - return self - - def run_middleware(self, type_of_middleware): - """Run route middleware. - - Arguments: - type_of_middleware {string} -- Type of middleware to be ran (before|after) - - Raises: - RouteMiddlewareNotFound -- Thrown when the middleware could not be found. - """ - # Get the list of middleware to run for a route. - for arg in self.list_middleware: - if ":" in arg: - middleware_to_run, arguments = arg.split(":") - # Splits "name:value1,value2" into ['value1', 'value2'] - arguments = arguments.split(",") - for index, argument in enumerate(arguments): - if argument.startswith("@"): - _, argument = argument.split("@") - arguments[index] = self.request.param(argument) - else: - middleware_to_run = arg - arguments = [] - - middleware_to_run = self.request.app().make("RouteMiddleware")[ - middleware_to_run - ] - if not isinstance(middleware_to_run, list): - middleware_to_run = [middleware_to_run] - - try: - for middleware in middleware_to_run: - located_middleware = self.request.app().resolve(middleware) - if hasattr(located_middleware, type_of_middleware): - getattr(located_middleware, type_of_middleware)(*arguments) - except KeyError: - raise RouteMiddlewareNotFound( - "Could not find the '{0}' route middleware".format(arg) - ) - - def compile_route_to_regex(self): - """Compile the given route to a regex string. - - Arguments: - route {string} -- URI of the route to compile. - - Returns: - string -- Compiled URI string. - """ - # Split the route - split_given_route = self.route_url.split("/") - # compile the provided url into regex - url_list = [] - regex = "^" - for regex_route in split_given_route: - # if not regex_route: - # continue - if "@" in regex_route: - if ":" in regex_route: - try: - regex += Route.route_compilers[regex_route.split(":")[1]] - except KeyError: - if self.request: - raise InvalidRouteCompileException( - 'Route compiler "{}" is not an available route compiler. ' - "Verify you spelled it correctly or that you have added it using the compile() method.".format( - regex_route.split(":")[1] - ) - ) - self._compiled_regex = None - self._compiled_regex_end = None - return - - else: - regex += Route.route_compilers["default"] - - regex += r"\/" - - # append the variable name passed @(variable):int to a list - url_list.append(regex_route.replace("@", "").split(":")[0]) - elif "?" in regex_route: - # Make the preceding token match 0 or more - regex += "?" - - if ":" in regex_route: - - try: - regex += Route.route_compilers[regex_route.split(":")[1]] + "*" - except KeyError: - if self.request: - raise InvalidRouteCompileException( - 'Route compiler "{}" is not an available route compiler. ' - "Verify you spelled it correctly or that you have added it using the compile() method.".format( - regex_route.split(":")[1] - ) - ) - self._compiled_regex = None - self._compiled_regex_end = None - return - - else: - regex += Route.route_compilers["default"] + "*" - - regex += r"\/" - - url_list.append(regex_route.replace("?", "").split(":")[0]) - else: - regex += regex_route + r"\/" - - self.url_list = url_list - regex += "$" - self._compiled_regex = re.compile(regex.replace(r"\/$", r"$")) - self._compiled_regex_end = re.compile(regex) - - return regex - - -class Get(BaseHttpRoute): - """Class for specifying GET requests.""" - - def __init__(self, route=None, output=None): - """Get constructor.""" - super().__init__() - self.method_type = ["GET"] - # self.list_middleware = [] - if route is not None and output is not None: - self.route(route, output) - - -class Head(BaseHttpRoute): - """Class for specifying HEAD requests.""" - - def __init__(self, route=None, output=None): - """Head constructor.""" - super().__init__() - self.method_type = ["HEAD"] - if route is not None and output is not None: - self.route(route, output) - - -class Post(BaseHttpRoute): - """Class for specifying POST requests.""" - - def __init__(self, route=None, output=None): - """Post constructor.""" - super().__init__() - self.method_type = ["POST"] - if route is not None and output is not None: - self.route(route, output) - - -class Match(BaseHttpRoute): - """Class for specifying Match requests.""" - - def __init__(self, method_type=["GET"], route=None, output=None): - """Match constructor.""" - super().__init__() - if not isinstance(method_type, list): - raise RouteException( - "Method type needs to be a list. Got '{}'".format(method_type) - ) - - # Make all method types in list uppercase - self.method_type = [x.upper() for x in method_type] - if route is not None and output is not None: - self.route(route, output) - - -class Put(BaseHttpRoute): - """Class for specifying PUT requests.""" - - def __init__(self, route=None, output=None): - """Put constructor.""" - super().__init__() - self.method_type = ["PUT"] - if route is not None and output is not None: - self.route(route, output) - - -class Patch(BaseHttpRoute): - """Class for specifying Patch requests.""" - - def __init__(self, route=None, output=None): - """Patch constructor.""" - super().__init__() - self.method_type = ["PATCH"] - if route is not None and output is not None: - self.route(route, output) - - -class Delete(BaseHttpRoute): - """Class for specifying Delete requests.""" - - def __init__(self, route=None, output=None): - """Delete constructor.""" - super().__init__() - self.method_type = ["DELETE"] - if route is not None and output is not None: - self.route(route, output) - - -class Connect(BaseHttpRoute): - """Class for specifying Connect requests.""" - - def __init__(self, route=None, output=None): - """Connect constructor.""" - super().__init__() - self.method_type = ["CONNECT"] - if route is not None and output is not None: - self.route(route, output) - - -class Options(BaseHttpRoute): - """Class for specifying Options requests.""" - - def __init__(self, route=None, output=None): - """Options constructor.""" - super().__init__() - self.method_type = ["OPTIONS"] - if route is not None and output is not None: - self.route(route, output) - - print( - "The Masonite development server is not capable of handling OPTIONS preflight requests." - ) - print("You should use a more powerful server if using the Option") - - -class Trace(BaseHttpRoute): - """Class for specifying Trace requests.""" - - def __init__(self, route=None, output=None): - """Trace constructor.""" - super().__init__() - self.method_type = ["TRACE"] - if route is not None and output is not None: - self.route(route, output) - - -class ViewRoute(BaseHttpRoute): - def __init__(self, method_type, route, template, dictionary): - """Class used for view routes. - - This class should be returned when a view is called on an HTTP route. - This is useful when returning a view that doesn't need any special logic and only needs a dictionary. - - Arguments: - method_type {string} -- The method type (GET, POST, PUT etc) - route {string} -- The current route (/test/url) - template {string} -- The template to use (dashboard/user) - dictionary {dict} -- The dictionary to use to render the template. - """ - - super().__init__() - self.method_type = method_type - self.route_url = route - self.template = template - self.dictionary = dictionary - self._compiled_url = self.compile_route_to_regex() - - def get_response(self): - return ( - self.request.app() - .make("ViewClass") - .render(self.template, self.dictionary) - .rendered_template - ) - - -class Redirect(BaseHttpRoute): - def __init__(self, current_route, future_route, status=302, methods=["GET"]): - """Class used for view routes. - - This class should be returned when a view is called on an HTTP route. - This is useful when returning a view that doesn't need any special logic and only needs a dictionary. - - Arguments: - method_type {string} -- The method type (GET, POST, PUT etc) - route {string} -- The current route (/test/url) - template {string} -- The template to use (dashboard/user) - dictionary {dict} -- The dictionary to use to render the template. - """ - super().__init__() - self.method_type = methods - self.route_url = current_route - self.status = status - self.future_route = future_route - self._compiled_url = self.compile_route_to_regex() - - def get_response(self): - return self.request.redirect(self.future_route, status=self.status) - - -class RouteGroup: - """Class for specifying Route Groups.""" - - def __new__( - cls, - routes=[], - middleware=[], - domain=[], - prefix="", - name="", - add_methods=[], - namespace="", - ): - """Call when this class is first called. This is to give the ability to return a value in the constructor. - - Keyword Arguments: - routes {list} -- List of routes. (default: {[]}) - middleware {list} -- List of middleware. (default: {[]}) - domain {list} -- String or list of domains to attach to all the routes. (default: {[]}) - prefix {str} -- Prefix to attach to all the route URI's. (default: {''}) - name {str} -- Base name to attach to all the routes. (default: {''}) - namespace {str} -- Namespace path to attach to all the routes. (default: {''}) - - Returns: - list -- Returns a list of routes. - """ - from .helpers.routes import flatten_routes - - cls.routes = flatten_routes(routes) - - if middleware: - cls._middleware(cls, *middleware) - - if add_methods: - cls._add_methods(cls, *add_methods) - - if domain: - cls._domain(cls, domain) - - if namespace: - cls._namespace(cls, namespace) - - if prefix: - cls._prefix(cls, prefix) - - if name: - cls._name(cls, name) - - return cls.routes - - def _middleware(self, *middleware): - """Attach middleware to all routes. - - Returns: - list -- Returns list of routes. - """ - for route in self.routes: - route.middleware(*middleware) - - return self.routes - - def _add_methods(self, *methods): - """Attach more methods to all routes. - - Returns: - list -- Returns list of routes. - """ - for route in self.routes: - route.method_type.append(*methods) - - return self.routes - - def _domain(self, domain): - """Attach a domain to all routes. - - Arguments: - domain {str|list|tuple} -- List of domains to attach to all the routes. - """ - for route in self.routes: - route.domain(domain) - - def _prefix(self, prefix): - """Prefix a string to all domain URI's. - - Arguments: - prefix {str} -- String to prefix to all Routes. - """ - for route in self.routes: - if route.route_url == "/": - route.route_url = "" - - route.route_url = prefix + route.route_url - route.compile_route_to_regex() - - def _name(self, name): - """Name to prefix to all routes. - - Arguments: - name {str} -- String to prefix to all routes. - """ - for route in self.routes: - if isinstance(route.named_route, str): - route.named_route = name + route.named_route - - def _namespace(self, namespace): - """Namespace of the controller for all routes - - Arguments: - namespace {str} -- String to add to find controllers for all Routes. - """ - if not namespace.endswith("."): - namespace += "." - for route in self.routes: - if isinstance(route.output, str): - route.e = False # reset any previous find_controller attempt - route.output = namespace + route.output - route._find_controller(route.output) - - -class Resource: - def __new__( - cls, - base="", - controller="", - only=["index", "create", "store", "show", "edit", "update", "destroy"], - names={}, - ): - if not names: - base_name = base.replace("/", ".") - if base_name[0] == ".": - base_name = base_name[1:] - - names = { - "index": "{}.index".format(base_name), - "create": "{}.create".format(base_name), - "store": "{}.store".format(base_name), - "show": "{}.show".format(base_name), - "edit": "{}.edit".format(base_name), - "update": "{}.update".format(base_name), - "destroy": "{}.destroy".format(base_name), - } - - routes = [] - - if "index" in only: - route = Get("{}".format(base), "{}@index".format(controller)) - if "index" in names: - route.name(names["index"]) - routes.append(route) - if "create" in only: - route = Get("{}/create".format(base), "{}@create".format(controller)) - if "create" in names: - route.name(names["create"]) - routes.append(route) - if "store" in only: - route = Post("{}".format(base), "{}@store".format(controller)) - if "store" in names: - route.name(names["store"]) - routes.append(route) - if "show" in only: - route = Get("{}/@id".format(base), "{}@show".format(controller)) - if "show" in names: - route.name(names["show"]) - routes.append(route) - if "edit" in only: - route = Get("{}/@id/edit".format(base), "{}@edit".format(controller)) - if "edit" in names: - route.name(names["edit"]) - routes.append(route) - if "update" in only: - route = Match(["PUT", "PATCH"]).route( - "{}/@id".format(base), "{}@update".format(controller) - ) - if "update" in names: - route.name(names["update"]) - routes.append(route) - if "destroy" in only: - route = Delete("{}/@id".format(base), "{}@destroy".format(controller)) - if "destroy" in names: - route.name(names["destroy"]) - routes.append(route) - - return routes diff --git a/src/masonite/routes/HTTPRoute.py b/src/masonite/routes/HTTPRoute.py new file mode 100644 index 000000000..a6783522c --- /dev/null +++ b/src/masonite/routes/HTTPRoute.py @@ -0,0 +1,284 @@ +import re +import os + +from ..utils.str import modularize, removeprefix +from ..exceptions import InvalidRouteCompileException +from ..facades import Loader +from ..controllers import Controller +from ..exceptions import LoaderNotFound + + +class HTTPRoute: + def __init__( + self, + url, + controller=None, + request_method=["get"], + name=None, + compilers=None, + controllers_locations=["app.http.controllers"], + controller_bindings=[], + **options, + ): + if not url.startswith("/"): + url = "/" + url + + self.url = url + self.controllers_locations = controllers_locations + self.controller = controller + self.controller_class = None + self.controller_method = None + self._domain = None + self._name = name + self.request_method = [x.lower() for x in request_method] + self.list_middleware = [] + self.e = None + self.compilers = compilers or {} + self._find_controller(controller) + self.controller_bindings = controller_bindings + self.compile_route_to_regex() + + def match(self, path, request_method, subdomain=None): + + route_math = ( + re.match(self._compiled_regex, path) + or re.match(self._compiled_regex_end, path) + ) and request_method.lower() in self.request_method + + domain_match = subdomain == self._domain + + return route_math and domain_match + + def get_name(self): + return self._name + + def matches(self, path): + return re.match(self._compiled_regex, path) or re.match( + self._compiled_regex_end, path + ) + + def match_name(self, name): + return name == self._name + + def name(self, name): + self._name = name + return self + + def domain(self, subdomain): + self._domain = subdomain + return self + + def to_url(self, parameters={}): + + # Split the url into a list + split_url = self.url.split("/") + + # Start beginning of the new compiled url + compiled_url = "/" + + # Iterate over the list + for url in split_url: + if url: + # if the url contains a parameter variable like @id:int + if "@" in url: + url = url.replace("@", "").split(":")[0] + if isinstance(parameters, dict): + compiled_url += str(parameters[url]) + "/" + elif isinstance(parameters, list): + compiled_url += str(parameters.pop(0)) + "/" + elif "?" in url: + url = url.replace("?", "").split(":")[0] + if isinstance(parameters, dict): + compiled_url += str(parameters.get(url, "/")) + "/" + elif isinstance(parameters, list): + compiled_url += str(parameters.pop(0)) + "/" + else: + compiled_url += url + "/" + + # The loop isn't perfect and may have an unwanted trailing slash + if compiled_url.endswith("/"): + compiled_url = compiled_url[:-1] + + # The loop isn't perfect and may have 2 slashes next to eachother + if "//" in compiled_url: + compiled_url = compiled_url.replace("//", "/") + + return compiled_url + + def _find_controller(self, controller): + """Find the controller to attach to the route. Look for controller (str or class) in all + specified controllers_location. + + Arguments: + controller {string|object} -- String or object controller to search for. + + Returns: + None + """ + if controller is None: + return None + # If the output specified is a string controller e.g. "WelcomeController@show" + elif isinstance(controller, str): + if "@" in controller: + controller_path, controller_method_str = controller.split("@") + else: + controller_path = controller + controller_method_str = "__call__" + + controller_path = modularize(controller_path).split(".") + if len(controller_path) > 1: + controller_name = controller_path.pop() + prefix_path = ".".join(controller_path) + else: + controller_name = controller_path[0] + prefix_path = "" + # build a list of all locations where the controller can be found + # if the controller is defined such as auth.WelcomeController, append the prefix path to + # the locations + locations = list( + map( + lambda loc: f"{loc}.{removeprefix(prefix_path, loc)}" + if prefix_path + else loc, + self.controllers_locations, + ) + ) + try: + self.controller_class = Loader.find( + Controller, locations, controller_name, raise_exception=True + ) + except LoaderNotFound as e: + self.e = e + print("\033[93mTrouble importing controller!", str(e), "\033[0m") + # Else it's a controller instance, we don't have to find it, just get the class + else: + if "." in controller.__qualname__: + controller_name, controller_method_str = controller.__qualname__.split( + "." + ) + else: + controller_name = controller.__qualname__ + controller_method_str = "__call__" + try: + self.controller_class = Loader.get_object( + controller.__module__, controller_name, raise_exception=True + ) + except LoaderNotFound as e: + self.e = e + print("\033[93mTrouble importing controller!", str(e), "\033[0m") + + # Set the controller method on class. This is a string + self.controller_method = controller_method_str + + def get_response(self, app=None): + # Resolve Controller Constructor + if self.e: + print( + "\033[93mCannot find controller {}. Did you create this one?".format( + self.controller + ), + "\033[0m", + ) + raise SyntaxError(str(self.e)) + + if app: + controller = app.resolve(self.controller_class, *self.controller_bindings) + # resolve route parameters + params = self.extract_parameters(app.make("request").get_path()).values() + # Resolve Controller Method + response = app.resolve(getattr(controller, self.controller_method), *params) + return response + + return getattr(self.controller_class(), self.controller_method)() + + def middleware(self, *args): + """Load a list of middleware to run. + + Returns: + self + """ + for arg in args: + if arg and arg not in self.list_middleware: + self.list_middleware.append(arg) + + return self + + def compile_route_to_regex(self): + """Compile the given route to a regex string. + + Arguments: + route {string} -- URI of the route to compile. + + Returns: + string -- Compiled URI string. + """ + # Split the route + split_given_route = self.url.split("/") + # compile the provided url into regex + url_list = [] + regex = "^" + for regex_route in split_given_route: + if "@" in regex_route: + if ":" in regex_route: + try: + regex += self.compilers[regex_route.split(":")[1]] + except KeyError: + raise InvalidRouteCompileException( + 'Route compiler "{}" is not an available route compiler. ' + "Verify you spelled it correctly or that you have added it using the compile() method.".format( + regex_route.split(":")[1] + ) + ) + + else: + regex += self.compilers["default"] + + regex += r"\/" + + # append the variable name passed @(variable):int to a list + url_list.append(regex_route.replace("@", "").split(":")[0]) + elif "?" in regex_route: + # Make the preceding token match 0 or more + regex += "?" + + if ":" in regex_route: + + try: + regex += self.compilers[regex_route.split(":")[1]] + "*" + except KeyError: + if self.request: + raise InvalidRouteCompileException( + 'Route compiler "{}" is not an available route compiler. ' + "Verify you spelled it correctly or that you have added it using the compile() method.".format( + regex_route.split(":")[1] + ) + ) + self._compiled_regex = None + self._compiled_regex_end = None + return + + else: + regex += self.compilers["default"] + "*" + + regex += r"\/" + + url_list.append(regex_route.replace("?", "").split(":")[0]) + else: + regex += regex_route + r"\/" + + self.url_list = url_list + regex += "$" + self._compiled_regex = re.compile(regex.replace(r"\/$", r"$")) + self._compiled_regex_end = re.compile(regex) + + return regex + + def extract_parameters(self, path): + if not self.url_list: + return {} + + if (not path.endswith("/")) or path == "/": + matching_regex = self._compiled_regex + else: + matching_regex = self._compiled_regex_end + return dict(zip(self.url_list, matching_regex.match(path).groups())) diff --git a/src/masonite/routes/Route.py b/src/masonite/routes/Route.py new file mode 100644 index 000000000..c525b9b83 --- /dev/null +++ b/src/masonite/routes/Route.py @@ -0,0 +1,165 @@ +from .HTTPRoute import HTTPRoute +from ..utils.collections import flatten +from ..utils.str import modularize +from ..controllers import RedirectController + + +class Route: + + routes = [] + compilers = { + "int": r"(\d+)", + "integer": r"(\d+)", + "string": r"([a-zA-Z]+)", + "default": r"([\w.-]+)", + "signed": r"([\w\-=]+)", + } + controllers_locations = [] + + def __init__(self): + pass + + @classmethod + def get(self, url, controller, module_location=None, **options): + return HTTPRoute( + url, + controller, + request_method=["get"], + compilers=self.compilers, + controllers_locations=module_location or self.controllers_locations, + **options + ) + + @classmethod + def post(self, url, controller, **options): + return HTTPRoute( + url, + controller, + request_method=["post"], + compilers=self.compilers, + controllers_locations=self.controllers_locations, + **options + ) + + @classmethod + def put(self, url, controller, **options): + return HTTPRoute( + url, + controller, + request_method=["put"], + compilers=self.compilers, + controllers_locations=self.controllers_locations, + **options + ) + + @classmethod + def patch(self, url, controller, **options): + return HTTPRoute( + url, + controller, + request_method=["patch"], + compilers=self.compilers, + controllers_locations=self.controllers_locations, + **options + ) + + @classmethod + def delete(self, url, controller, **options): + return HTTPRoute( + url, + controller, + request_method=["delete"], + compilers=self.compilers, + controllers_locations=self.controllers_locations, + **options + ) + + @classmethod + def options(self, url, controller, **options): + return HTTPRoute( + url, + controller, + request_method=["options"], + compilers=self.compilers, + controllers_locations=self.controllers_locations, + **options + ) + + @classmethod + def default(self, url, controller, **options): + return self + + @classmethod + def redirect(self, url, new_url, **options): + return HTTPRoute( + url, + RedirectController.redirect, + request_method=["get"], + compilers=self.compilers, + controllers_locations=self.controllers_locations, + controller_bindings=[new_url, options.get("status", 302)], + **options + ) + + @classmethod + def permanent_redirect(self, url, new_url, **options): + return HTTPRoute( + url, + RedirectController.redirect, + request_method=["get"], + compilers=self.compilers, + controllers_locations=self.controllers_locations, + controller_bindings=[new_url, 301], + **options + ) + + @classmethod + def match(self, request_methods, url, controller, **options): + return HTTPRoute( + url, + controller, + request_method=request_methods, + compilers=self.compilers, + controllers_locations=self.controllers_locations, + **options + ) + + @classmethod + def group(self, *routes, **options): + inner = [] + for route in flatten(routes): + if options.get("prefix"): + if route.url == "" or route.url == "/": + route.url = options.get("prefix") + else: + route.url = options.get("prefix") + route.url + + route.compile_route_to_regex() + + if options.get("name"): + route._name = options.get("name") + route._name + + if options.get("domain"): + route.domain(options.get("domain")) + + if options.get("middleware"): + route.middleware(*options.get("middleware", [])) + + inner.append(route) + self.routes = inner + return inner + + @classmethod + def compile(self, key, to=""): + self.compilers.update({key: to}) + return self + + @classmethod + def set_controller_locations(self, *controllers_locations): + self.controllers_locations = list(map(modularize, controllers_locations)) + return self + + @classmethod + def add_controller_locations(self, *controllers_locations): + self.controllers_locations.extend(list(map(modularize, controllers_locations))) + return self diff --git a/src/masonite/routes/Router.py b/src/masonite/routes/Router.py new file mode 100644 index 000000000..8184e5cc3 --- /dev/null +++ b/src/masonite/routes/Router.py @@ -0,0 +1,93 @@ +from ..utils.collections import flatten +from ..exceptions import RouteNotFoundException + + +class Router: + def __init__(self, *routes, module_location=None): + self.routes = flatten(routes) + + def find(self, path, request_method, subdomain=None): + + for route in self.routes: + if route.match(path, request_method, subdomain=subdomain): + return route + + def matches(self, path): + for route in self.routes: + if route.matches(path): + return route + + def find_by_name(self, name): + for route in self.routes: + if route.match_name(name): + return route + + def route(self, name, parameters={}): + route = self.find_by_name(name) + if route: + return route.to_url(parameters) + raise RouteNotFoundException(f"Could not find route with the name '{name}'") + + def set_controller_locations(self, location): + self.controller_locations = location + return self + + def add(self, *routes): + self.routes.append(*routes) + self.routes = flatten(self.routes) + + def set(self, *routes): + self.routes = [] + self.routes.append(*routes) + self.routes = flatten(self.routes) + + @classmethod + def compile_to_url(cls, uncompiled_route, params={}): + """Compile the route url into a usable url: converts /url/@id into /url/1. + Used for redirection + + Arguments: + route {string} -- An uncompiled route like (/dashboard/@user:string/@id:int) + Keyword Arguments: + params {dict} -- Dictionary of parameters to pass to the route (default: {{}}) + Returns: + string -- Returns a compiled string (/dashboard/joseph/1) + """ + if "http" in uncompiled_route: + return uncompiled_route + + # Split the url into a list + split_url = uncompiled_route.split("/") + + # Start beginning of the new compiled url + compiled_url = "/" + + # Iterate over the list + for url in split_url: + if url: + # if the url contains a parameter variable like @id:int + if "@" in url: + url = url.replace("@", "").split(":")[0] + if isinstance(params, dict): + compiled_url += str(params[url]) + "/" + elif isinstance(params, list): + compiled_url += str(params.pop(0)) + "/" + elif "?" in url: + url = url.replace("?", "").split(":")[0] + if isinstance(params, dict): + compiled_url += str(params.get(url, "/")) + "/" + elif isinstance(params, list): + compiled_url += str(params.pop(0)) + "/" + else: + compiled_url += url + "/" + + compiled_url = compiled_url.replace("//", "") + # The loop isn't perfect and may have an unwanted trailing slash + if compiled_url.endswith("/") and not uncompiled_route.endswith("/"): + compiled_url = compiled_url[:-1] + + # The loop isn't perfect and may have 2 slashes next to eachother + if "//" in compiled_url: + compiled_url = compiled_url.replace("//", "/") + + return compiled_url diff --git a/src/masonite/routes/__init__.py b/src/masonite/routes/__init__.py new file mode 100644 index 000000000..39981be49 --- /dev/null +++ b/src/masonite/routes/__init__.py @@ -0,0 +1,3 @@ +from .Route import Route +from .HTTPRoute import HTTPRoute +from .Router import Router diff --git a/src/masonite/scheduling/CanSchedule.py b/src/masonite/scheduling/CanSchedule.py new file mode 100644 index 000000000..4cdf44c7b --- /dev/null +++ b/src/masonite/scheduling/CanSchedule.py @@ -0,0 +1,10 @@ +class CanSchedule: + def call(self, command): + command_class = CommandTask(command) + self.app.make("scheduler").add(command_class) + return command_class + + def schedule(self, task): + task_class = task + self.app.make("scheduler").add(task_class) + return task_class diff --git a/src/masonite/scheduling/CommandTask.py b/src/masonite/scheduling/CommandTask.py new file mode 100644 index 000000000..f8b2e27e9 --- /dev/null +++ b/src/masonite/scheduling/CommandTask.py @@ -0,0 +1,13 @@ +from .Task import Task +import subprocess + + +class CommandTask(Task): + + run_every_minute = True + + def __init__(self, command=""): + self.command = command + + def handle(self): + subprocess.call(self.command.split(" ")) diff --git a/src/masonite/scheduling/Task.py b/src/masonite/scheduling/Task.py new file mode 100644 index 000000000..97c92771e --- /dev/null +++ b/src/masonite/scheduling/Task.py @@ -0,0 +1,148 @@ +import pendulum + + +class Task: + + run_every = False + run_at = False + run_every_hour = False + run_every_minute = False + twice_daily = False + run_weekly = False + + _date = None + + name = "" + + def __init__(self): + """ + Should only be on the child class. Also needs to be resolved by the container. + """ + + pass + + def every(self, time): + self.run_every = time + return self + + def every_minute(self): + self.run_every = "1 minute" + return self + + def every_15_minutes(self): + self.run_every = "15 minutes" + return self + + def every_30_minutes(self): + self.run_every = "30 minutes" + return self + + def every_45_minutes(self): + self.run_every = "45 minutes" + return self + + def hourly(self): + self.run_every = "1 hour" + return self + + def daily(self): + self.run_every = "1 day" + return self + + def weekly(self): + self.run_every = "1 week" + return self + + def monthly(self): + self.run_every = "1 month" + return self + + def at(self, run_time): + self.run_at = run_time + return self + + def at_twice(self, run_time): + self.twice_daily = run_time + return self + + def daily_at(self, run_time): + return self.daily().at(run_time) + + def handle(self): + """Fires the task""" + + pass + + def should_run(self, date=None): + """If the task should run""" + + # set the date + self._set_date() + + return self._verify_run() + + def _set_date(self): + if not self._date: + self._date = pendulum.now() + if hasattr(self, "timezone"): + self._date.in_timezone(self.timezone) + + def _verify_run(self): + if self.run_every: + length, frequency = self.run_every.split(" ") + + if frequency in ("minute", "minutes"): + time = int(length) + if self._date.minute == 0 or self._date.minute % time == 0 or time == 1: + return True + + elif frequency in ("hour", "hours"): + time = int(length) + if self._date.hour % time == 0 and self._date.minute == 0: + return True + + elif frequency in ("day", "days"): + time = int(length) + if self._date.day_of_year % time == 0 and ( + self._date.hour == 0 + and self._date.minute == 0 + or self._verify_run_at() + ): + return True + elif frequency in ("month", "months"): + time = int(length) + if ( + self._date.month % time == 0 + and self._date.day == 1 + and ( + self._date.hour == 0 + and self._date.minute == 0 + or (self._date.day == 0 and self._verify_run_at()) + ) + ): + return True + + elif self.run_at: + return self._verify_run_at() + + if self.run_every_minute: + return True + elif self.run_every_hour: + if self._date.hour / 1 == 1: + return True + elif self.twice_daily: + if self._date.hour in self.twice_daily: + return True + + return False + + def _verify_run_at(self): + if self._date.minute < 10: + minute = f"0{self._date.minute}" + else: + minute = self._date.minute + + if f"{self._date.hour}:{minute}" == self.run_at: + return True + + return False diff --git a/src/masonite/scheduling/TaskHandler.py b/src/masonite/scheduling/TaskHandler.py new file mode 100644 index 000000000..56fa4f511 --- /dev/null +++ b/src/masonite/scheduling/TaskHandler.py @@ -0,0 +1,30 @@ +import pendulum +import inspect + + +class TaskHandler: + def __init__(self, application, tasks=None): + if tasks is None: + tasks = [] + + self.tasks = tasks + self.application = application + + def add(self, *tasks): + self.tasks += list(tasks) + + def run(self, run_name=None): + app = self.application + for task_class in self.tasks: + # Resolve the task with the container + if run_name and run_name != task_class.name: + continue + + if inspect.isclass(task_class): + task = app.resolve(task_class) + else: + task = task_class + + # If the class should run then run it + if task.should_run(): + task.handle() diff --git a/src/masonite/scheduling/__init__.py b/src/masonite/scheduling/__init__.py new file mode 100644 index 000000000..23d8fd7be --- /dev/null +++ b/src/masonite/scheduling/__init__.py @@ -0,0 +1,4 @@ +from .CanSchedule import CanSchedule +from .CommandTask import CommandTask +from .Task import Task +from .TaskHandler import TaskHandler diff --git a/src/masonite/scheduling/commands/MakeTaskCommand.py b/src/masonite/scheduling/commands/MakeTaskCommand.py new file mode 100644 index 000000000..7d8d54c21 --- /dev/null +++ b/src/masonite/scheduling/commands/MakeTaskCommand.py @@ -0,0 +1,46 @@ +"""New Task Command """ +import os +import inflection +from cleo import Command +from os.path import exists + +from ...utils.filesystem import make_directory, get_module_dir, render_stub_file +from ...utils.location import base_path +from ...utils.str import as_filepath + + +class MakeTaskCommand(Command): + """ + Create a new task + task + {name : Name of the task you want to create} + {--d|--directory=? : Override the directory to create the task in} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + output = render_stub_file(self.get_stub_task_path(), name) + + relative_file_name = os.path.join( + self.option("directory") or as_filepath(self.app.make("tasks.location")), + f"{name}.py", + ) + filepath = base_path(relative_file_name) + + if exists(relative_file_name): + return self.line_error( + f"Task already exists at: {relative_file_name}", style="error" + ) + + make_directory(filepath) + with open(filepath, "w") as fp: + fp.write(output) + + self.info(f"Task Created ({relative_file_name})") + + def get_stub_task_path(self): + return os.path.join(get_module_dir(__file__), "../../stubs/scheduling/Task.py") diff --git a/src/masonite/scheduling/commands/ScheduleRunCommand.py b/src/masonite/scheduling/commands/ScheduleRunCommand.py new file mode 100644 index 000000000..976456c60 --- /dev/null +++ b/src/masonite/scheduling/commands/ScheduleRunCommand.py @@ -0,0 +1,21 @@ +""" A ScheduleRunCommand Command """ +import pendulum +import inspect +from cleo import Command + +from ..Task import Task + + +class ScheduleRunCommand(Command): + """ + Run the scheduled tasks + schedule:run + {--t|task=None : Name of task you want to run} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + return self.app.make("scheduler").run() diff --git a/src/masonite/scheduling/commands/__init__.py b/src/masonite/scheduling/commands/__init__.py new file mode 100644 index 000000000..d211b748b --- /dev/null +++ b/src/masonite/scheduling/commands/__init__.py @@ -0,0 +1,2 @@ +from .MakeTaskCommand import MakeTaskCommand +from .ScheduleRunCommand import ScheduleRunCommand diff --git a/src/masonite/scheduling/providers/ScheduleProvider.py b/src/masonite/scheduling/providers/ScheduleProvider.py new file mode 100644 index 000000000..541dbf5d5 --- /dev/null +++ b/src/masonite/scheduling/providers/ScheduleProvider.py @@ -0,0 +1,20 @@ +""" A ScheduleProvider Service Provider """ +from ...providers import Provider + +from ..commands import MakeTaskCommand, ScheduleRunCommand +from ..TaskHandler import TaskHandler + + +class ScheduleProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + self.application.make("commands").add( + MakeTaskCommand(self.application), ScheduleRunCommand(self.application) + ) + + self.application.bind("scheduler", TaskHandler(self.application)) + + def boot(self): + pass diff --git a/src/masonite/scheduling/providers/__init__.py b/src/masonite/scheduling/providers/__init__.py new file mode 100644 index 000000000..67d89c4f8 --- /dev/null +++ b/src/masonite/scheduling/providers/__init__.py @@ -0,0 +1 @@ +from .ScheduleProvider import ScheduleProvider diff --git a/src/masonite/sessions/Session.py b/src/masonite/sessions/Session.py new file mode 100644 index 000000000..0922d7e0b --- /dev/null +++ b/src/masonite/sessions/Session.py @@ -0,0 +1,147 @@ +import json + + +class Session: + def __init__(self, application, driver_config=None): + self.application = application + self.drivers = {} + self._driver = None + self.driver_config = driver_config or {} + self.options = {} + self.data = {} + self.added = {} + self.flashed = {} + self.deleted = [] + self.deleted_flashed = [] + + def add_driver(self, name, driver): + self.drivers.update({name: driver}) + + def driver(self, driver): + return self.drivers[driver] + + def set_configuration(self, config): + self.driver_config = config + return self + + def get_driver(self, name=None): + if name is None: + return self.drivers[self.driver_config.get("default")] + return self.drivers[name] + + def get_config_options(self, driver=None): + if driver is None: + return self.driver_config[self.driver_config.get("default")] + + return self.driver_config.get(driver, {}) + + # Start of methods + def start(self, driver=None): + self.data = {} + self.added = {} + self.flashed = {} + self.deleted = [] + self.deleted_flashed = [] + started_data = self.get_driver(name=driver).start() + self.data = started_data.get("data") + self.flashed = started_data.get("flashed") + return self + + def get_data(self): + data = self.data + data.update(self.added) + data.update(self.flashed) + for deleted in self.deleted: + if deleted in data: + data.pop(deleted) + for deleted in self.deleted_flashed: + if deleted in data: + data.pop(deleted) + return data + + def save(self, driver=None): + return self.get_driver(name=driver).save( + added=self.added, + deleted=self.deleted, + flashed=self.flashed, + deleted_flashed=self.deleted_flashed, + ) + + def set(self, key, value): + try: + if isinstance(value, (dict, list, int)) or ( + isinstance(value, str) and value.isnumeric() + ): + value = json.dumps(value) + except json.decoder.JSONDecodeError: + pass + + return self.added.update({key: value}) + + def increment(self, key, count=1): + return self.set(key, str(int(self.get(key)) + count)) + + def decrement(self, key, count=1): + return self.set(key, str(int(self.get(key)) - count)) + + def has(self, key): + return key in self.added or key in self.flashed + + def get(self, key): + if key in self.flashed: + value = self.flashed.get(key) + + try: + if value is not None: + value = json.loads(value) + except json.decoder.JSONDecodeError: + pass + self.flashed.pop(key) + self.deleted_flashed.append(key) + return value + + value = self.get_data().get(key) + try: + if value is not None: + value = json.loads(value) + except json.decoder.JSONDecodeError: + pass + return value + + def pull(self, key): + key_value = self.get(key) + self.delete(key) + return key_value + + def flush(self): + self.deleted += list(self.get_data().keys()) + + def delete(self, key): + self.deleted.append(key) + if key in self.flashed: + self.flashed.pop(key) + + def flash(self, key, value): + """Add temporary data to the session. + + Arguments: + key {string} -- The key to set as the session key. + value {string} -- The value to set in the session. + """ + try: + if isinstance(value, (dict, list, int)) or ( + isinstance(value, str) and value.isnumeric() + ): + value = json.dumps(value) + except json.decoder.JSONDecodeError: + pass + + self.flashed.update({key: value}) + + def all(self): + """Get all session data. + + Returns: + dict + """ + return self.get_data() diff --git a/src/masonite/sessions/__init__.py b/src/masonite/sessions/__init__.py new file mode 100644 index 000000000..c99ac5368 --- /dev/null +++ b/src/masonite/sessions/__init__.py @@ -0,0 +1 @@ +from .Session import Session diff --git a/src/masonite/snippets/auth/controllers/ConfirmController.py b/src/masonite/snippets/auth/controllers/ConfirmController.py deleted file mode 100644 index cea5df288..000000000 --- a/src/masonite/snippets/auth/controllers/ConfirmController.py +++ /dev/null @@ -1,69 +0,0 @@ -"""The ConfirmController Module.""" -import datetime - -from masonite.auth import Auth, MustVerifyEmail -from masonite.auth.Sign import Sign -from masonite.managers import MailManager -from masonite.request import Request -from masonite.view import View -from masonite.helpers import config - - -class ConfirmController: - """The ConfirmController class.""" - - def __init__(self): - """The ConfirmController Constructor.""" - pass - - def verify_show(self, view: View, auth: Auth): - """Show the Verify Email page for unverified users. - - Arguments: - request {masonite.view.view} -- The Masonite view class. - request {masonite.auth.auth} -- The Masonite Auth class. - - Returns: - [type] -- [description] - """ - return view.render("auth/verify", {"app": config("application"), "Auth": auth}) - - def confirm_email(self, request: Request, view: View, auth: Auth): - """Confirm User email and show the correct response. - - Arguments: - request {masonite.request.request} -- The Masonite request class. - request {masonite.view.view} -- The Masonite view class. - request {masonite.auth.auth} -- The Masonite Auth class. - - Returns: - [type] -- [description] - """ - sign = Sign() - token = sign.unsign(request.param("id")) - if token is not None: - tokenParts = token.split("::") - if len(tokenParts) > 1: - user = auth.auth_model.find(tokenParts[0]) - if user.verified_at or user.verified_at is None: - timestamp = datetime.datetime.fromtimestamp(float(tokenParts[1])) - now = datetime.datetime.now() - timestamp_plus_10 = timestamp + datetime.timedelta(minutes=10) - - if now < timestamp_plus_10: - user.verified_at = datetime.datetime.now() - user.save() - - return view.render( - "auth/confirm", {"app": config("application"), "Auth": auth} - ) - - return view.render("auth/error", {"app": config("application"), "Auth": auth}) - - def send_verify_email(self, manager: MailManager, request: Request): - user = request.user() - - if isinstance(user, MustVerifyEmail): - user.verify_email(manager, request) - - return request.redirect("/home") diff --git a/src/masonite/snippets/auth/controllers/HomeController.py b/src/masonite/snippets/auth/controllers/HomeController.py deleted file mode 100644 index e756b7cfd..000000000 --- a/src/masonite/snippets/auth/controllers/HomeController.py +++ /dev/null @@ -1,17 +0,0 @@ -"""The HomeController Module.""" - -from masonite.auth import Auth -from masonite.request import Request -from masonite.view import View - - -class HomeController: - """Home Dashboard Controller.""" - - def __init__(self): - pass - - def show(self, request: Request, view: View, auth: Auth): - if not auth.user(): - request.redirect("/login") - return view.render("auth/home") diff --git a/src/masonite/snippets/auth/controllers/LoginController.py b/src/masonite/snippets/auth/controllers/LoginController.py deleted file mode 100644 index 6296ce7cd..000000000 --- a/src/masonite/snippets/auth/controllers/LoginController.py +++ /dev/null @@ -1,66 +0,0 @@ -"""A LoginController Module.""" - -from masonite.auth import Auth -from masonite.request import Request -from masonite.validation import Validator -from masonite.view import View - - -class LoginController: - """Login Form Controller.""" - - def __init__(self): - """LoginController Constructor.""" - pass - - def show(self, request: Request, view: View): - """Show the login page. - - Arguments: - request {masonite.request.Request} -- The Masonite request class. - view {masonite.view.View} -- The Masonite view class. - - Returns: - masonite.view.View -- Returns the Masonite view class. - """ - if request.user(): - return request.redirect("/home") - - return view.render("auth/login") - - def store(self, request: Request, auth: Auth, validate: Validator): - """Login the user. - - Arguments: - request {masonite.request.Request} -- The Masonite request class. - auth {masonite.auth.auth} -- The Masonite auth class. - validate {masonite.validator.Validator} -- The Masonite Validator class. - - Returns: - masonite.request.Request -- The Masonite request class. - """ - errors = request.validate( - validate.required(["email", "password"]), - validate.email("email"), - ) - - if errors: - return request.back().with_errors(errors).with_input() - - if auth.login(request.input("email"), request.input("password")): - return request.redirect("/home") - - return request.back().with_errors({"email": ["Email or password is incorrect"]}) - - def logout(self, request: Request, auth: Auth): - """Log out the user. - - Arguments: - request {masonite.request.Request} -- The Masonite request class. - auth {masonite.auth.auth} -- The Masonite auth class. - - Returns: - masonite.request.Request -- The Masonite request class. - """ - auth.logout() - return request.redirect("/login") diff --git a/src/masonite/snippets/auth/controllers/PasswordController.py b/src/masonite/snippets/auth/controllers/PasswordController.py deleted file mode 100644 index 421533afe..000000000 --- a/src/masonite/snippets/auth/controllers/PasswordController.py +++ /dev/null @@ -1,79 +0,0 @@ -"""A PasswordController Module.""" - -import uuid - -from masonite import env, Mail, Session -from masonite.auth import Auth -from masonite.helpers import config, password as bcrypt_password -from masonite.request import Request -from masonite.view import View -from masonite.validation import Validator -from config.auth import AUTH - - -class PasswordController: - """Password Controller.""" - - def forget(self, view: View, auth: Auth): - return view.render("auth/forget", {"app": config("application"), "Auth": auth}) - - def reset(self, view: View, request: Request, auth: Auth): - token = request.param("token") - user = AUTH["guards"]["web"]["model"].where("remember_token", token).first() - if user: - return view.render( - "auth/reset", - {"token": token, "app": config("application"), "Auth": auth}, - ) - - def send(self, request: Request, session: Session, mail: Mail, validate: Validator): - errors = request.validate(validate.required("email"), validate.email("email")) - - if errors: - return request.back().with_errors(errors) - - email = request.input("email") - user = AUTH["guards"]["web"]["model"].where("email", email).first() - - if user: - if not user.remember_token: - user.remember_token = str(uuid.uuid4()) - user.save() - message = "Please visit {}/password/{}/reset to reset your password".format( - env("SITE", "http://localhost:8000"), user.remember_token - ) - mail.subject("Reset Password Instructions").to(user.email).send(message) - - session.flash( - "success", - "If we found that email in our system then the email has been sent. Please follow the instructions in the email to reset your password.", - ) - return request.redirect("/password") - - def update(self, request: Request, validate: Validator): - errors = request.validate( - validate.required("password"), - # TODO: only available in masonite latest versions (which are not compatible with Masonite 2.2) - validate.strong( - "password", - length=8, - special=1, - uppercase=1, - # breach=True checks if the password has been breached before. - # Requires 'pip install pwnedapi' - breach=False, - ), - ) - - if errors: - return request.back().with_errors(errors) - - user = ( - AUTH["guards"]["web"]["model"] - .where("remember_token", request.param("token")) - .first() - ) - if user: - user.password = bcrypt_password(request.input("password")) - user.save() - return request.redirect("/login") diff --git a/src/masonite/snippets/auth/controllers/RegisterController.py b/src/masonite/snippets/auth/controllers/RegisterController.py deleted file mode 100644 index 6feec9a76..000000000 --- a/src/masonite/snippets/auth/controllers/RegisterController.py +++ /dev/null @@ -1,77 +0,0 @@ -"""The RegisterController Module.""" - -from masonite.auth import Auth, MustVerifyEmail -from masonite.managers import MailManager -from masonite.request import Request -from masonite.validation import Validator -from masonite.view import View - - -class RegisterController: - """The RegisterController class.""" - - def __init__(self): - """The RegisterController Constructor.""" - pass - - def show(self, view: View): - """Show the registration page. - - Arguments: - Request {masonite.request.request} -- The Masonite request class. - - Returns: - masonite.view.View -- The Masonite View class. - """ - return view.render("auth/register") - - def store( - self, - request: Request, - mail_manager: MailManager, - auth: Auth, - validate: Validator, - ): - """Register the user with the database. - - Arguments: - request {masonite.request.Request} -- The Masonite request class. - - Returns: - masonite.request.Request -- The Masonite request class. - """ - errors = request.validate( - validate.required(["name", "email", "password"]), - validate.email("email"), - validate.strong( - "password", - length=8, - special=1, - uppercase=1, - # breach=True checks if the password has been breached before. - # Requires 'pip install pwnedapi' - breach=False, - ), - ) - - if errors: - return request.back().with_errors(errors).with_input() - - user = auth.register( - { - "name": request.input("name"), - "password": request.input("password"), - "email": request.input("email"), - } - ) - - if isinstance(user, MustVerifyEmail): - user.verify_email(mail_manager, request) - - # Login the user - if auth.login(request.input("email"), request.input("password")): - # Redirect to the homepage - return request.redirect("/home") - - # Login failed. Redirect to the register page. - return request.back().with_input() diff --git a/src/masonite/snippets/auth/templates/auth/base.html b/src/masonite/snippets/auth/templates/auth/base.html deleted file mode 100644 index 4a5f39fc0..000000000 --- a/src/masonite/snippets/auth/templates/auth/base.html +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - {{ config('application.name') }} - - - {% block css %}{% endblock %} - - - - - - -- {% block content %}{% endblock %} -- - - - - diff --git a/src/masonite/snippets/auth/templates/auth/confirm.html b/src/masonite/snippets/auth/templates/auth/confirm.html deleted file mode 100644 index 909a2c71d..000000000 --- a/src/masonite/snippets/auth/templates/auth/confirm.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends 'auth/base.html' %} - -{% block content %} ---{% endblock %} diff --git a/src/masonite/snippets/auth/templates/auth/error.html b/src/masonite/snippets/auth/templates/auth/error.html deleted file mode 100644 index 8cca33c18..000000000 --- a/src/masonite/snippets/auth/templates/auth/error.html +++ /dev/null @@ -1,12 +0,0 @@ -{% extends 'auth/base.html' %} - -{% block content %} ---Verified-- Thank you for confirming your email. Click here to go to the home page. ----{% endblock %} diff --git a/src/masonite/snippets/auth/templates/auth/forget.html b/src/masonite/snippets/auth/templates/auth/forget.html deleted file mode 100644 index e99f4937f..000000000 --- a/src/masonite/snippets/auth/templates/auth/forget.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends 'auth/base.html' %} - -{% block content %} ---Verifying Error-- Confirming email failed. Click here to go home ----{% endblock %} diff --git a/src/masonite/snippets/auth/templates/auth/home.html b/src/masonite/snippets/auth/templates/auth/home.html deleted file mode 100644 index 7967ebe18..000000000 --- a/src/masonite/snippets/auth/templates/auth/home.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends 'auth/base.html' %} - -{% block content %} ------ {% if session().has('error') %} --- {{ session().get('error') }} -- {% endif %} - {% if session().has('success') %} -- {{ session().get('success') }} -- {% endif %} -----Password Reset
-- ----{% endblock %} diff --git a/src/masonite/snippets/auth/templates/auth/login.html b/src/masonite/snippets/auth/templates/auth/login.html deleted file mode 100644 index def049c36..000000000 --- a/src/masonite/snippets/auth/templates/auth/login.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends 'auth/base.html' %} - -{% block content %} ---Dashboard
-This is a dashboard. Hello {{ auth().name }}
---{% endblock %} \ No newline at end of file diff --git a/src/masonite/snippets/auth/templates/auth/register.html b/src/masonite/snippets/auth/templates/auth/register.html deleted file mode 100644 index 2236f3dd2..000000000 --- a/src/masonite/snippets/auth/templates/auth/register.html +++ /dev/null @@ -1,43 +0,0 @@ -{% extends 'auth/base.html' %} - -{% block content %} -----------Login
-- {% if bag().any() %} - {% for error in bag().get_errors() %} --- {{ error }} -- {% endfor %} - {% endif %} - ---{% endblock %} diff --git a/src/masonite/snippets/auth/templates/auth/reset.html b/src/masonite/snippets/auth/templates/auth/reset.html deleted file mode 100644 index 201aadd13..000000000 --- a/src/masonite/snippets/auth/templates/auth/reset.html +++ /dev/null @@ -1,34 +0,0 @@ -{% extends 'auth/base.html' %} - -{% block content %} -----------Register
-- {% if bag().any() %} - {% for error in bag().get_errors() %} --- {{ error }} -- {% endfor %} - {% endif %} - ---{% endblock %} \ No newline at end of file diff --git a/src/masonite/snippets/auth/templates/auth/verify.html b/src/masonite/snippets/auth/templates/auth/verify.html deleted file mode 100644 index 1d588a787..000000000 --- a/src/masonite/snippets/auth/templates/auth/verify.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends 'auth/base.html' %} - -{% block content %} -----------Reset Password
-- {% if bag().any() %} - {% for error in bag().get_errors() %} --- {{ error }} -- {% endfor %} - {% endif %} - ---{% endblock %} diff --git a/src/masonite/snippets/auth/templates/auth/verifymail.html b/src/masonite/snippets/auth/templates/auth/verifymail.html deleted file mode 100644 index b3841b731..000000000 --- a/src/masonite/snippets/auth/templates/auth/verifymail.html +++ /dev/null @@ -1,288 +0,0 @@ - - - - - - ---Verify-- Please check your email and follow the link to verify your email. If - you need us to resend the email. Click here --Document - - - - --
- - diff --git a/src/masonite/snippets/exception.html b/src/masonite/snippets/exception.html deleted file mode 100644 index 4fd55e622..000000000 --- a/src/masonite/snippets/exception.html +++ /dev/null @@ -1,281 +0,0 @@ - - - - - - -- -- - ----
- -- -- --
-- -- Please confirm your email address by clicking the link below. - -- -- We may need to send you critical information about our service and it is important that we have an accurate email address. - -- -- Confirm Email Address - -- -- — Masonite Framework - -- - {{ tb[0].__name__ }} > {{ exception }} - - - - - - - - -- -- ---- -----StackTrace
---- {% for stack in stacktrace %} - {% if 'site-packages' in stack[0] %} - {% set local_file_background_color = 'inherit' %} - {% else %} - {% set local_file_background_color = '#d9ffc2' %} - {% endif %} ---- {% endfor %} -{{ stack[0] }}, line {{ stack[1] }} in {{ stack[2]}}
- - {% for i, line in enumerate(open(stack[0])) %} - {% if stack[1] - 5 <= i <= stack[1] + 5 %} - - {% if i == stack[1] - 1 %} -- {{ i + 1 }}. {{ line }} -- {% elif stack[1] - 3 <= i <= stack[1] + 1 %} -- {{ i + 1 }}. {{ line }} -- {% else %} -- {{ i + 1 }}. {{ line }} -- {% endif %} - - {% endif %} - {% endfor %} - -
- --- - - \ No newline at end of file diff --git a/src/masonite/snippets/exceptions/css/go-icon.png b/src/masonite/snippets/exceptions/css/go-icon.png deleted file mode 100644 index fcc918cca9e2cc625626b05e9676c7fcd6998f80..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15196 zcmd73^;?ut^FMymttj0f3er*nl7iA5qSVqI(k1K)sC0LChjg=xgh)#_2m-P+NH4pe z#pik6|H1dVzWWPY=bV`{GiP2ibK(xuR97G&q$LCZfJ900l{NriqW{GN@Nv<9{`yQ^ z1AsWI(yN!cJ`4NH1U|acx2WTm+99@B$Q;tJ>dj(#tSUCn=O24N9-Bqvb3e*(G+V9X zR~k@OWDeok6UPsABn-W^t7@7y=-- - -Environment
-Request
- -------Request Method
-----{{ app.make('Request').environ['REQUEST_METHOD'] }}
-------Path
-----{{ app.make('Request').path }}
-------Inputs
---- {% for key in app.make('Request').all() %} --{{ key }}={{ app.make('Request').input(key) }}
- {% else %} -None
- {% endfor %} -------Parameters
---- {% for key in app.make('Request').url_params %} --{{ key }}={{ app.make('Request').param(key) }}
- {% else %} -None
- {% endfor %} --- -----Headers
---- {% for header, value in app.make('Environ').items() %} - {% if header.startswith('HTTP_') %} --{{ header }}: {{ value }}
- {% endif %} - {% else %} -None
- {% endfor %} -
- -Container
- -------Provider Classes
----- - {% for provider, class in app.providers.items() %} -{{ provider }} - - {% if platform.system() != 'Windows' %} - - {{ class }} - {% endif %} -
- {% endfor %} - -------Environ
---- {% for key, value in app.make('Environ').items() %} --{{ key }} - {{ value }}
- {% endfor %} - -------Routes
---- {% for route in app.make('WebRoutes') %} --URL: {{ route.route_url }}
-Method: {{ route.method_type }}
-Name: {{ route.named_route }}
-Module: {{ route.module_location }}
-Route Middleware: - {% for middleware in route.list_middleware %} - {{ middleware }}, - {% else %} - None - {% endfor %} -
- -
- {% endfor %} - -------Service Providers
---- {% for provider in providers %} --{{ provider }}
- {% endfor %} --- - -----Static File Directories
---- {% for directory in app.make('staticfiles') %} --- {{ directory}} -
- {% else %} -None
- {% endfor %} -ot78X*_DS zXgpmu>29S5MJ~Z Q@XT6KCAdu$M|+ zy;_nj0sx6O*(TfU=^e(84TL&)9_4^q1d}U&g1iw!y}Mt$`=BN|q=gx}ruf9#vs{{5 z0I IwHk!l@^k3uLc=Elca?uFGaP0PqzDs 4)l-bWyEVWAO*o7s84;v6dJM6+y;Kqcz9a}(5nL}XirQ8vr?qw{Z|qI-XYp1ZL= zAp5wn9ud@MjybPq%>FG22cUhDj+8TpWe eB+Yc{{sIw(%|# zBxErctHnxjZK8h^GnbD<&ahv3v0?)vnlag(UxRY`KJ*&z;&_zwg0`9X@epZT4*~l) zzwb}FHW}lXexn#kLT$NS!V5BQI3C=e+EB&;sKh9zcGPPN49nY|M##$vbwOcTwfCO9 zlq8Agq5_=%@ LgpW#_tJ7e9To%5P)#HB^<4 ?g`^sU z)N{yeSp?@La?I@<01iJ))$ksW9bQiJ=+XQoXkGB?X?qE8{1%D< %jLhrg!(_d-;0+qe|tUL+*PTzF8Pqv2=^Znw_7il>nbe3Ayj>ea6Gjao@`t#X&|C zhCf0rEHQH5DGc9#K6-zmDD?a7Ch 3#<#sSWX83rkE*2SQbbh1#|p(h*+rrJBHx zJ&HImt0^1>_?sg0RiqAaLx`>4LrrZoE_=g*0ee+q&WpN%7}ERVAXusYZm8JWSfnZ7 zd{hk*QIa^}vygex%X**Y1uG@I|40asX=G@|UV1`#eajU5l*J?tT&8hCJr5u_Ytt_| zj(vYB;i}1Yz*RnKAJ5i0GGnh%O}T{i`S*|zCx-Z(;mZ1-{(T`WF9A>;TnA&4n14dC z;3~>mregm&=>suDet`5QhT_4)yl?BL8TzQH3F3H9?=KvbjVa }l;FH0Wa#srGH(N91-dh+QBEGyAd&R0Gu;{!R5P^?|H2!lN6a_kk1N zhx9lMB mxalInWG7h^oE IViYOtbZ%~lvxXLL-XjgZ?brEUAp}x z!lo(0yS+x=iH|2btkXxC9=!6~fJ57x%?caOx#5ZqlgZoxbRy?2a>-O_4_{CG9>l!- z__!tqoxgAyhzLKd-R1K(4qd0&!fUAO;6#~K*G`=DZ#*0?o@emuFoM?&b72DdxBeCB zPpLhgAgo_iVKqw8{J=<;E<* p${}DE3f2H z1nWEBw2#OB7W>4%4_bZOmVo_TFdb|5 p%PE%>G}xdp6#{Yb0sO=QhN@fLY7uB>%yEwVUPq$6NigWwA!3Q#^@Je#uR5 zFaPS}-q&~wD-q{=K0RN;`$imHbn#DD@O*jUY5!CzV~_P*agH;4AW^Gc2rSd?&1a6& z((=CPnUAd`#-YAbo5lWIa`_oCyn?zm!pw3yI&|vU2-7$%zbKFS(fA%K@}yh=A#ZHM zIo$)Q4GXrLQcEg7H_C0YLqZlaz?v{(-1|Jw15tc^?EaHolQ+%z%AFC{3-a8I^vO1V zQK<$cS6B!3$N(A%A+Hd-=@*eO(&t;pf$)+vs-~y+<`ni
kz+nf6Drlsy!ntVjCYT^k((2;xJ@--=<8t^aLFz zkKybQI>cy^b`OQ7E@ECH5BVW2t;ckp?r%q8Dju|pT!$O{XlWV4nk61#DorczQSViE zC1yTY%+{Bz$nLz `& zS@r_&L)nZoA`qA+`udhSPUHFI%Tgpre_JF()f-qRrEz_*U!*i^5FU-YBVX!mpi9@F z{( 218-+3^+?U21M)1@_gwp{Q(JRQpwm#g=> zrR-X-sa1Z}%cDypk#zlAnfomdT1}HJUAT+E?= %LFi z#1kIrr-t*gVq1S?HoUZ^g8jSPFGcd(*3_obDQgH*?AK`NhH5aTG|fJ{OwH>%>r7?N z&nSu^r*iDpgl{@(UM-tN4F-t}iR>_XZ)^t%fpd!otyes~=c7Mw^Qr+;x_f0cnc)bm zC M{0|?eP?3x3NlpfB-M2L|t6Rmg5~tHO zalq7U !pu!{ift Ftw3c6i7+pYA znw~P+nx@!#*h`|v@XF>(`{MQ4=CO08o*eQu^VjdHyr<<)Q9Erw%@SX3=okW+B~>uT zi^#$nvPH@m3xl__?k7XvcDf)P;swWiX9F?d7sELH00RYfi~2{FXNs9ATB@gbaG4(y zlsU6r|BM(xlBmOacox2u!!%XPI*rwmJ;%QGQw5Iz4S54?iHec0ZT3iMF$HF&Ojc7u zG>BRrozaqM%zZv($C$$G+i`c8FJ^mkkP4J?7|v+lx#TGxkjopU27c34afSNIYih{4 z*$b0KTNx-#*vjGdLM;9~>S$*N^n6>h-TY1tg^*cLlwG `ZRj0XrN@|Z``+X9X`Vb=o#X>*p}1pp2j90AjZOs2mQ2#0bmSz z1DaURrOZHW<`8Dnm&Ngf1bfJpQ3H_e)!UM0qDi+?D8lwvews5<_t{hC^l%Ko=hH;e zNY_Ou0jw{P*L$)g=w%y@E)f9ZBDc9DIV#*jC1~K_Noal{gFpiQA(a?_i60 p(Dec2(p^DJ1~Wf2!<6w^KWUI?=(YH=JE52||~bzMrGLb$HNw zh}IjB_l=!PDxizro*a8^&TZUpyBr01%YQgrjZ#KS6t(WLYPT|h0K1j1+0JHh4&- zw}c%xE^dR1Y=HD0W2#$8yi4`3lAzBJJzW9-RzY5T6LmDRh3Y8_sz~Sh{*AHgCk5(| z2|iWEl7tByaavWRZ~ER~qw& ?1y52V|thlG$h`;0A3X>OHcay491QX*Ra=+fr9%;wKO zZ`)o=X8?Q7n(yK->A;-Wz*>^BV8Hgo5jgGJ-t&sGdjRA@9e6IpTlfnlR797asY}jy ziv@J(Y8AVHXpv)G*mL?CTGC;KFVJdMSlXRx*j-DnlK+X8)Wrp0{u$8I(en@27xfCL z4B8Y y^(l?5 -BzJDsFCv4FMd!E6U@`3>ma0r(dKlO_dvfxB*U zrkzREP~Q{x9W{Li=aG#62F_ieYz$GOPG=H&7{HOevU7wroYz|j86l2za?&0BCJjJl zxkgX2 8MXwU_wDeo7MvW z5IaCpH3Y^HK;B+K8^pL!7{JNX#if>10+`d_PNBG5DK46ey3M7O>?zgvTV9uQ9yEMo z<&QI;8=@J1N3G*qmSX_SQFTcpF-qVIXqVE1q`LuDpItJ-1NLq~nbX!R|44p1LdkbD z5R21C9qW$EA@*p^3OwGOY80Y@?(==M41F>qMBcYuT*p8%_M_`qL&@F{{EziySr^NM zG!itQsDT0iH;(K@xbF@Vs^msJ?;!waKgdCC{#Zf`H9vn*;dtKqNg2(URin~R`qcbZ zx$F>0wRDVoCz7&~Tl-Hm9}NeGA~c @9uwv*CIo;94#UacEmMXlUdPTf zPBfF(+#f2ux1O}FqEg1L_HT7D0U6HMJ5D!Bx0hhG`VCzSK;}gq7zGk-AK@$q0A>cy z3voMkfNxeuU2JhxjsgIFqy5^}$7G=o?BF7;gaZJD?&j7v&H~a%zVualIz6;h>c`yB zR&73D_*_^MJzyu1*YS+cE(6shPoWC{GRjuF=?n?yx2XK&LoE#Sa $m&t~RI1+= Ao+byLV9GT=pkAMAjuX#WTZkS&|5*Tln%6aZ)amV~xzB8-cg<4%I zj07 {1mWFiis{EfDDQYF~DsmFK?V z6;ir+vmJ4LWT}#!eR>4%o@c<1 fD91Qqbw5{Dk z>In*D)Yt*KbaJIDg0 ;zZL_x +u+_A}yot>` hHSnB|1~VmPt6THvc1#n+AZ6m2Atr zkVV?;%@jN`KsFUntU_GL+Z1L1j&??aeE-$^h5+Vu6^+TeIR5)FRN_BsAFu=MbeSnm zn3`>EzEc0!N&r(i<0{Gkxb4mv{?}-4^EeYo =NX7wjfF z6~zv8U;=0rELG0yhnP&sOMhUSLL(!9j5>T$w6TtlETfE&{_-DbOXmzhW=#K~O)flk zSQH?fg7^t!dyqjkM`BKfg727S?XY?`|A&db+)fiFuVKIqL7*?%H+t(WhN>&ao@(CJ z%1S=enj4S)(;l1wHpu&iuPIkFCL+PLh@FeZquYPF+{u!SVA#3+8bH(Y7p%+JruseZ z|Iui>SpVBEWqC-t6vo_m;j1PBh#wN;pOw~Icj!=0MBIe4fy(yV!pIB^J?qQoTeEk| zBq4~kelOb$yL5*OLNU^n;)ucpfv29@IiVpE*{ByM9VbyHJ;Fm~1 zH~{k*bec$;ksx!@PvpPTr9D616rskjFSU(!sg5LrOe`hi{m19WBHfop_t9j!nOob5 zXd#>0IM^|R(Dr3aPaGIgK@ j-(+H~ zaTc|XNXMP1K$6s;|Ft8RMRAvFYQ^g7DRDCncEFG><#_j>+az%$nE0lkJ9?cVDpr|* zZikVE)|+dgTuc}`252nc9DUk1x~)zJ3a``>&vanl-Z@=91n%53*D}%|V|4oP5qtdN zu2mL5ZqxWq5$B(SCp;M&1;?YACXPs*ah^}3L$qew*Xc!F0FA_Tv3KI!6+HS-q9hkP zBi`*inUR>&{|FOj61G|w)n1WQ<#^tiXoI=us97c&;9UGqmVFz=;g32-SG;c;zD`36 zCCQl2MoBsEMEM!Wrgpu{Zgf?h4Gi1W%gzwU{OEXI@z2a58bFb5>QC#C1~Q1%h;+$+ zS f>nRsE(#!w{q=o*0uArb&t%y~ zxsEIJu0;BQJWq{1ZwTJ814ZtLEgg)1-B2LZuO_6TtBDODGPpHdU d{y{fBa(sG0o0Y z|E0#|HN{kZomx~xp)r0-+dB*jK&GNtZ2=WmNe02F%}Lh6Xsq;58oF bir7qv}ZJw-PUNTc?4u#i!NEzt3mungABA^GzIMihuKj1y30+iwmpKIYX?s qn`4?djjFD27oOGWT 9U{~+KKbl@xx@09{&u^@X2j!P7DYoag=Y3<`w~L zPs<@w_a%c^mir9@S}#(?ks+~*A{_!?$!YY@nYJ{txrl<3NS?PPLb+6~h%O^D*%$L@ ztACXqZSt1I$Yb&IK?w|M^4kaOz)a&hj&}#kDfU|LP*jq} sW{c8Z<*Ckc1(dLAR96dO2*7jx4C&&L3iVB9cWSq3iEa|h#EjZshF z@Zea_{(6PB0i+@DOMj`uzjtEwi=7l<8;iP7-!S$JtGyJS)PHXio*xhjY1mNfcr` z!;7#T;Z+_ct19mp?ffCifadH3o(r^7Vf`XW0G%GQkc@|@<)X*!XBBlqsQVX9Zyq%n zl+&U%(+(Q;iys10jfJ&_TR)JO(t(I*kuq!%4t0l%hGr4ui9Vbo!W;R{_xHLnYK*LF z+*SVgxHZfk&`kOKnn9G~2HUjz>ynmkP)4c$ddi`_&x&CN3U%J7d+j7_Tzd@4emBV- zr;7z?i|04bGCtp0 I%$yaQ($;y1fw1e<1#A_((>8bKWCh>hnie6c75Ytg@0Ob1B$ ;uy8NO=s`e^*%!{)uek~ zLE<|0n!TLGigZ7A?pr1Ln{RZw!TpKD_KP}0t7kQ2TnOW{YSe-T#;}quHjuJ>8{=b~vD^!@qGsoO=Zdb^jJUTZSL?BI{sxT=o=Y397K) z{{_m}>0~Ypy&E^y`{Aw3DKyq9*!^26J_*<2RN<6y6x0%oWwjEGSL5dfGG^yrSTB48 zES3Ht@b90v K>>u;qS(x3FZA@B#o2&zibDT&e0G2Ip+%}7E2CRzN$rIRiWWOvhp zQ{*B-e83x(@*G7|A2$MSs1`bC&Kf(YY3~P{gIS-r`a|+3H8-FVwFEO1cO=4|6G%&1 z)F ^EK#3>VaTzX%c_p3OOx5nIshzE#ed5tejEM2fiyrI+)W?FIvy2o6tpH-B{8 zD`#Viq!O@Rkk-(qV9dA7KAc!^M64b-l)rM`&EM$<`?9gko$F8l &LIv~TiGEn~hi%$cZ( Tm7=B z!VSyC-8JmxZ_ZWmLj1x)^nug<7%PmUcNl+=Q `<$T8Rr_=9hp4H!hPCpGmAIa#+XgBy0QeY0)3^z2J+nqy) zCtY)NpZZCqoO3!%cr1Df-GX|XD)DMCfgck5f``ru+EOvdn%Mb*S*JYNj$u3vPdhV1 zO0Vo<)KH{qGg$c(;P(DSQpU~q0KZUc)X?8BY28-wZ#xP}K6#g-rxij7E_RlFfe1p3 zj@5xw@07{Cn#|t=YA#wVCiY`8JvnVP2TDEA1>BbjQCVdVVg4jBS1}{W28#7i*>4Gi ziMKH^(^^kV91Zf%bZC)Dr?1o%f2qVZUF#C81#qX87@RAM8Y$O9-7+_tYmZ}^7ezbT zYfrCDgb)>$AHw(5%CV0G!rdleEiux$KS5&MW$Z~L!I>|~!?F7d4WPyIpxZf9F9Ph2 zp`%nRBHO?E8h?AKp!v#=Yb`5(9uMHA-%dYl5I`W7l$L#{aD6`JxibCz-HDnei+59= zzf?`nQjQz=L%t{?bW>4s@ojv)!LHa^mvF7suZZzB;BvTtLg>1}=DJsMRg)wBo#etF zw2wCe&}17M9I$*5kp1JAF727x L%;MFP=|` _4+1Z`{c` z+P6#h7gk58TsFCFCuo5S?T><~OE!gRKb$>Gvq#;hz_OKl>({-nAqu_B-ZJS#*ZB1@ z<=q;4yQ{5gNAX`zg)ZS%%K1(rdRQ$fJneNzo#Py^vPF#vZhwH^K@}s`L9W{oQJPK^ zm)*g%hGUQw7JA^9m9nJ;zJoQW-QM)rx*1W5mRQT9C+i>mG)6oVe=b#k-;YPq%ouUv zl^Uv-xSc{;P@T#;HG%!Aw>D`n`gJ5*$|>SLKV6nj_dgRMgjWqm3XT=KLE74Mj$7?a zl?Sg)T|-HtI{j)RdSaiGC{k-2uhGsXkEz9RoJ*idCYAOmQ^}p&v?XQsMNqMQX6;kE z2W4UjNug=06KfT5bI0wXM|jiqfrTqmRtz~fCK^*bNn{p0f@O@k=jq_NzAZ3EQ&Dh| zjWyy68;WZu>(Y4hAXEuSPFn6{pTEx}biNCINGjbEuXG0RZh5TGw>@@TrC?v(&DNCv z?p!htdXiS~2Kox)SDPc9X@}f!)7ZpZ4OmUaDAS4`eXa5tsdrnI*P2&jvb#XXFQ`KQ zzNZZ`B*=Zee1f%wj4FF%)SOAs??Z{bGB=CCf1KQ?ONzb%gr4l+vZ% WkC7$A?`c N>$De&C_GVT8 zb|%X@aD!logl{a{-Y3x>Gq7wsXna6@*H3Szzma{Y4Pqh~^K=nUeOF$%do%wbH@GBj zmix2buk8d2bQMbWK5lNUWHx`isIlu^3id8$odWl={L8#I9kh8a5BXzyg6zBWW%! Ka!{ZRu_;RwsOKoUXIa@9NdHSzas=3! DHM$ UG8SG2z`t@C$9@ zBQ}k#kc)$^Hk;Ps8p>T#a>>7&~NBgRIqbY^0bj)spP8qq$;nC!~LW0Xs|s$EN?S zwWisT2eP%t#s5R#AGRuqlZ#?c3?++fX~j?7Rmlnqj9+<)#5Fd&Zp~y1Cia-d80g?C zN!?^;CX_QQmP&8CN{s!p(KmbVhi@oq Ag_kp5ReLeu5#=iA35?dR)n>HXNAEW8M^ z58etLyTKhLNW1vc#%OZ!V!UmSrj~bYHQg3@yy3OQ;dfg_PA{~5_0m{;;q86RT?Z`r zLOc|od(b8$=RJdG%-?^`$I+#4kIXI?>@rbp<4YW_Tp)ikW#8J=yaea2=e`^2|6?Iu z&zP&;MIP#Dkn;5IKFaewo+lG3@HEI9*Ex2(&3(2FlC9)BSd6U2DtlpT@*(p|FWqDL zw^FT(%oe%MGrccuLfl+FLB75%iFm=&XKjF?eO#TT?Kx-pJ;s;IOr}1cZ=aI19jH@9 zs@5~DH3IgFvBR;j6(WW&me4J@o3V`Y?_!NI37^Eqxa;%IE%DLu++E)5BuTE4Cv#r! zzcCx_?_6~UVr{=LN!f7)H_qVn&K$Hs2JBqO!DivM@;Ki%xFdw#MsoSR5qLZ0)Vsw+ zaa^11!AUAS&0eq3^Q%5+*3*z|bbX~Fn>Dr!T~T}Zo*pvYte)0Lh_S_ThI}^={V|X) zBf==0N$Zp?OR?^^1Dala@ksJ9tL |QnaeY&zOL~m-BFF}2DIOKEk(1!tIKuSI!58H#W!+PpWZx^v1R?$ z`fJnmI5}xYhp{sYo;Wnun%8A9vA3wo<)67}P}=E<@gQtfxBV(6 4hfAwMz5Wn32K6B!!Zroev82gs$=v+7h zCFshOwYVx?hebhnn1ikQ?EX$uF*{bs%;-zdstQf|gA+abL|-92xa8Eu%_Nx LcnL zOO*dL`6M94Ug@8a_28_xB$G^jm=HPeHQdQhl81vodA8JC#>b`w (H0w7<-F52oX<5`u511Q|bDVS{G^gw 7+V}8#gv6|zOO&ZH+q9R{S2Y%Z6Kxp z_u8<`d2KrZf*(NU>YXqq^GlMzgf`F%oi}u99|au5?|7Zlw0y4oA+Mv_VYPoW&jPBr z5!94hNgz;aI=p)*sksT$%fXhIGUB#=(GB?(pas2r&wWqw)DgYi(IQADq0t!$5K(xg z*uK9rL6BuotRWJWnr*u*_IM~T_48rh2)I}h6@ibkjy!@1cdV4C?6TC$sSCdM0k>7} zmq{e;iwZv zsJ<#;Q7&w09Vuoabj?#Rv441;wJX}q8Q?3(DJcq)#E*Kw(4xMmkg*Wfry)VWEbUy; zR;@qn5Q5 *K!i} TsT0J;rk$ zKQ%vM5Muu=ibdGEQW`VYUIW+SNIw-j3zHpt9X-MBCQAq&K#WhOlw1VG<{!N_vx0x? zV|Jab?#eb5k#v)bxlO22Si#>&RLku@zklI!zQ?_&g-a Z%P4-D3rgNc-6@(t!9xxF(qZ)V{Vi%G|7}Y-0;)@y{!p8@J~w5YYVout2S1 zPW2P%dbL{RmYTqb#bewh%X3{F)eF7MkLvhk?WxHZrA7jy&pth%eqaAOK{aMEB_zwj z^ijx^*Ecu0F&D<4MSQG$xepxVJ;A?^ukx&jHA7+-l%c-SCs|_>GoKz%ji*w{dv!Vt zII2H<9MU3XF%fiXLNm9cE^i=HFhvr~RyZeZ4SN*Q0vCkN`J95M0u-suFz&}vJFJZR zN`ml7Wt}Br!g@-q)dXwIUg1a#FDdnFQMxMAVbQSs_Ol?f_+F WqthSP{vadav+7H$# zdDkS-hJF?m5(_FGYRew1EGt%nzwHhs@X6cbD9&S1@z1n^p`TZ`v}(-F_mL5gJ(I;W zasEMldiZ&i5PA0?TR=ck^srUy+Mk*RBg}-L_j_874$}=k{=-M0sj-j*i(&t1)_$4? z7zAb8_UDnvW?Jt>=wlRI&dn3p@e?D^1{dra3 z1mp7MwUE2|MG`%aYN!aCtyRom!_(78@-G1WL_rg#XHSPrU=XFyi{ELj-R!fGd)H4{ z=^>yZUQ*TY#>#a&^nD)62`roGnUX)dIyiuvyO11pFylK;|J@Z=TULVawmw)TC0gjO z`D3j278+QYQX*rK{7!paZFs?)ER1fmOiT{*7co* SL1e)K|-m1Kqb^m z(6o9(_dv8h>j4pD=t=Lptqf7~RML^EBaLw3DxI7;er1eOVJ ^GZ#A8OvR(L?xCAznV2 * !lH=BL#Ez*w)y;_-Q&Z>cWPI_R()nF zSIYc}fIlAT`nOQ5_Q8n))eUMhz}DniXecr}LUxFzgP`>|RX-#vhrg%px%cJKBDEg+ zQ-~DDrt4$Ztur`tzp1+7*`%kFhLu6k*~or@+Wy_Q65O7o8OgT(oxABK#0Mb= K#FrssbJsg}_=PWwbEL zS2lcKS``~V3YsGDxF=B5B3o4g(^T9Ni!($}xY!ONQ~tVuLB2#_h3 X4FJ~6>kH_?ndz3k{OHOYhvbVb%krCxO8PWsX*8UPwg_<`2F2)GIQ6jSYC6!o ztunw>!H<`iBa6|+IE&uOVT6JJaLzqoe ssEL|;I*D4mqJ*TpJ1^e-uq1q zh`eZ@&c`(V XE#~csPe0}`q*G6@AWAew&((J$gOr_0KI+j#%+ `v=mm1>IC$NnwPc>IMzL$%4tlT92sK!F1=+Wrj56}K6F#&ejnFIs)jEAPuQHea zwT|G&47B+5yI~_SVD0sUdGKKlql=-KsrtZ!Y~tAR;v6~yq36?0?AFC2-zYGFhXYxq zE=65z)KXlRf=x`xcG5@_ !*Kh<>reN13Lf`*i?c{p4khH|l~i=pXhN2L6Y zmElP>zO;zwWZ*O2?)0RshQ^Y03_xGVp>DrB)n_QkjHW5LX;bpDV;t-Ls;_w$E7&MK zz_4<1{YdMBb3Xcg(X2E}s aX6Eq$tGV3Y<8qVCxH&zv7!4Qe>(EDLldJ+kD9I;3;>B2g z%!Gww0k&_}b#FlH>%%(iYzs~I)10JBow<8TzCgP+O#Z|loxDstcR|NGljx}C5!Fya z_~4B{0Qv6z;;gw?AuV!!V6NeqBU!)KOdj<29jV_!iv%K5J$@GddRlVy!sz!?ZMs|Z zfnCNV*)UO-x;xXc&14862B669lwS63&?91m#wI$8(q&$hZ3KBP=uZFThWkDFLh={A z(wJ|OX8hS!uVd=rlhMd|76pajI;PZE^8l9TEAg4s4)CN*diW3@2>|nye)+XQH5!h5 zt6k}Nccz40OX@n^`*^oYYv;WAbIGDKJ-WC)Evc97+D{=%N4<*P(5?Ao_N2Qt&qj5q zXLP4;mI!X=H5+dBdQR-7?ki!foQa*wu}8j(*Ffz@ceyU6ObW#25%D5>!KBk+q)H=H zK #sG}4|&gPm+ z<|rwJ3N>Sfa>;FSE5dI&b p&oo zjJ=(evxsJ|-ck~xXTzBiriiG}t&h=NsNVE2A`JkUdr>_Bs6CnJ12_XjukfI!02Ty- z?DRW)jDF1V0ER>*!-=apa0WS0ga(1IW{f~0$seFYJpms-3Qlvfx=s`7=Y`XBMLFs_ z23i8Xes&Qwz$L=zFe$>Hg!a-jGlgOq7?A-oKqo>O R11R^Xf3?60(r_y{7NHiLa&^JIB7{Ejtu;6eCoydSu zg13LSU OjnOE5BnpYNMwuI$h*B4ckVb~)NVL&+tUV=|PNa~4?^wS-u_phD z#aPk+BArS*Or-{V?;hTlN~Z? rZg@}$)w zeg7eKaaLRczTncj$K@5DtWLTm=oVSuwg}fvEx5cX(vy^@ZLyD|tZ)gWT^jTLi1kI= zfWp$6(xQF3xx nC;8>Ke%9W5l}*c!PHdLTMt6^=zu$1@$3v9TbAq*1kR#nYnbj-9+v6s^ zIgpyZpC*3Zb^l^5W_rL!#-e`SO(d&{RlV1r$^PtE@Jm{)n~*%=Bea8mJTn24?fkO2 zL%SKSRcIZ4GBzM`Uv0^n5oApK$5%r@ibUrL%};X>Y 4{T{P&Qg8P@J$XnSV>y?^sd^*T{>HHV7WVA% zr0KzD%c}UO(tr#BPhN;u7m^RlSD7vnxUOJ1RAWGOs8sKf&P?&6?s=hnImTVto_rX= z1%1y_sXP^_oJx9n-(_zx1e=?Ar{9^Y%-yoYq)68Cr|mIy^Pc-=Yt mKYlA z?pAexF0XVTmouK|g&j2hjad~0D^MG g>z`LT5%ImwQ!vB!9x8T%Sst2%3^n?@S_ zt|Tw-H>1RdoA5pT&u+6CFoN`wiV|E%PD14)L8a{*H4SjiICs&)WA|Wh8 zn!^*h6RKigmJb=3N3WRE4iP^~qsz%77n&Skq*_<}xN6&3bFb+*H?c2UvK$H&Ccy-M z{cIPc!(4KcgIrqCMlA@KjLr2tl-rxZE=io)g>9Eb?(W*tQ~?wxR6Y;A6L~xkCY>-p zA+)-6W4CH~MSG{hz$};{+r-44O}gRcpGj|;&sLut-zb<2vtNjwFiV_TAPv{ro{`B( zu^}t~kfVXgC;JwuqGnW}IK9j4!i1>>Y12_Me?*0Pevt8s 1`>+i z@yMRjR3&Qy#=QI}XDk0!%V)M?>@xgPaiW%zV#?I@N8b6`BPzN``*?B+#u4QH=FyP< z5o~InTV9u6+d%D5;?z00=DS;&b%kkk)z^mdSGg~NHy=z}G)@wnOmmJR5&C>pz3No; zVgW9D63Ys6*SLBjH$L)|Q#4FacVw=-$$@(5#xOz>wQzi^{B+cQQ@9W2cBO#pxg2I_ z#*X33zI=f5N-j&DJaPD~5UuyoInySiXl|uV&}x)>KUGa$&@i}O(&onEXmX6JzA*9_ z(=K#)LtXT9@NsQWIsIj$Y&P`K#7 fpV!jwoCX|# zgCB^98Gd2fHEQA5)}^7R>}!U?-8VlacH!>T_qrHua^trRF6ema$PXx%JXvI&+YTvQ zIT4yM=A6?#o`=hvTzqJ$xP3HSN#&7>yHe?}b@8n7q_}5IT ?#0c&)p%CcB8G&O0N`J0E^})+VAY zWoGBduc6-eYJ015qHz2YnS$~FW7j$+mI)RpN?}hC8;zVNE~ho`RpWHX&wEVGgtLRI z>@i=oUUJ!nOW22Ngy(t&exMSW+_9qN8tseZ;pM8aD*@QfLDRv{ZEDkIu{(703zKh; z?-C<`)1*%ct9OjZdrGH$8h0!**zZ Zy>h5ZfkLuvOpDla6d7KAN*U%bijNQ<)BU85y0U;#TH-kL5{qbotAq@>QTD7jB z;`G(c e}?}JEJ7`x{Wd>WFskFRw{TE6L4>)IelSFTyVUNU?= zWB&ygq#kVJ<^y_XeW~Yk)Mlrr1`GL$$QE$1m;i`T9{54115^*bo2%D4vBfRhQ8}{f z$x&G6@Q}}AjCjqu*sHPqDA`_a2H~FD4`ZRM$8oc3iLI5!70n*t;)9%K(5GiwJyD+S z+fZ
7PmtK>CXqSdf`p(SEM4Kaffzg1w8lT$=rvcqWP@wE z8UgCNl)YA? 8xpLxIEcKKkY1M}^+Uw9rByl%JBsJjqfowJSVxlw5o zqm`5{$(nKLY_+&xLej3#nT7?eC1;Y`usiSMnVSvFUr8;dnR&LY7uMGVuqQ}3LAnw) zQ#w;EoSRc)eB2emE2@y-H)oqFl6uowwn+tr?fg;-53*_z)|oOgr=mTw#cy+?%(m)- z*Spim61bjx&$F0>phkS!vlv$e;Y`f@6yM*ZlsoWvVFUSnvBiA<=Ou0; &SC&AQc4 zc~40O!k>e*@@sN_ %`Wr=(C6k{8QIf#ya9AqFzKlpgle}+?uyrkZjks@_y)bQBYw+ zrK>kXJy6+p= J zcZN$}RKSEukJ8iXleAIhl(=4kda;|IiC#xQWkmgtb#b;SqRr*M7nqy N!-Jp#FdFL4RMK@H=MBz!IQnZjY8Tt#8Zf PkC45!lhu6-kGTH;mfIZT diff --git a/src/masonite/snippets/exceptions/css/style.css b/src/masonite/snippets/exceptions/css/style.css deleted file mode 100644 index 3d98e436c..000000000 --- a/src/masonite/snippets/exceptions/css/style.css +++ /dev/null @@ -1,5 +0,0 @@ - -.breadcrumb{background-color:transparent;border-bottom:1px solid #eee;padding-top:12px;padding-bottom:12px;margin-bottom:40px}nav.navbar.navbar-default{margin-bottom:0}@media (min-width:992px){.product h2{margin-top:0}}.navbar .navbar-brand{font-size:24px;line-height:18px}.reviewer-name{margin-right:10px}.site-footer{padding:20px 0;text-align:center}@media (min-width:768px){.site-footer h5{text-align:left}}.site-footer h5{color:inherit;font-size:16px}.site-footer .social-icons a:hover{opacity:1}.site-footer .social-icons a{display:inline-block;width:32px;border:none;font-size:20px;border-radius:50%;margin:4px;color:#fff;text-align:center;background-color:#798FA5;height:32px;opacity:.8;line-height:32px}@media (min-width:768px){.site-footer .social-icons{text-align:right}}.btn.write-review{float:right;margin-top:-6px} -.stack-row:nth-child(even) { - background-color: white !important; -} \ No newline at end of file diff --git a/src/masonite/snippets/exceptions/obj_loop.html b/src/masonite/snippets/exceptions/obj_loop.html deleted file mode 100644 index bd39033b6..000000000 --- a/src/masonite/snippets/exceptions/obj_loop.html +++ /dev/null @@ -1,43 +0,0 @@ - -\ No newline at end of file diff --git a/src/masonite/snippets/scaffold/command.html b/src/masonite/snippets/scaffold/command.html deleted file mode 100644 index 5229a2839..000000000 --- a/src/masonite/snippets/scaffold/command.html +++ /dev/null @@ -1,15 +0,0 @@ -"""A {{ class }} Command.""" -from cleo import Command - - -class {{ class }}(Command): - """ - Description of command - - command:name - {argument : description} - """ - - def handle(self): - pass - diff --git a/src/masonite/snippets/scaffold/controller.html b/src/masonite/snippets/scaffold/controller.html deleted file mode 100644 index 21ca72ac6..000000000 --- a/src/masonite/snippets/scaffold/controller.html +++ /dev/null @@ -1,21 +0,0 @@ -"""A {{ class }} Module.""" - -from masonite.request import Request -from masonite.view import View -from masonite.controllers import Controller - - -class {{ class }}(Controller): - """{{ class }} Controller Class.""" - - def __init__(self, request: Request): - """{{ class }} Initializer - - Arguments: - request {masonite.request.Request} -- The Masonite Request class. - """ - self.request = request - - def show(self, view: View): - pass - diff --git a/src/masonite/snippets/scaffold/controller_resource.html b/src/masonite/snippets/scaffold/controller_resource.html deleted file mode 100644 index 2c274146d..000000000 --- a/src/masonite/snippets/scaffold/controller_resource.html +++ /dev/null @@ -1,61 +0,0 @@ -""" A {{ class }} Module """ - -from masonite.controllers import Controller - - -class {{ class }}(Controller): - """Class Docstring Description - """ - - def show(self): - """Show a single resource listing - ex. Model.find('id') - Get().route("/show", {{ class }}) - """ - - pass - - def index(self): - """Show several resource listings - ex. Model.all() - Get().route("/index", {{ class }}) - """ - - pass - - def create(self): - """Show form to create new resource listings - ex. Get().route("/create", {{ class }}) - """ - - pass - - def store(self): - """Create a new resource listing - ex. Post target to create new Model - Post().route("/store", {{ class }}) - """ - - pass - - def edit(self): - """Show form to edit an existing resource listing - ex. Get().route("/edit", {{ class }}) - """ - - pass - - def update(self): - """Edit an existing resource listing - ex. Post target to update new Model - Post().route("/update", {{ class }}) - """ - - pass - - def destroy(self): - """Delete an existing resource listing - ex. Delete().route("/destroy", {{ class }}) - """ - - pass diff --git a/src/masonite/snippets/scaffold/job.html b/src/masonite/snippets/scaffold/job.html deleted file mode 100644 index 6f628b3ff..000000000 --- a/src/masonite/snippets/scaffold/job.html +++ /dev/null @@ -1,16 +0,0 @@ -"""A {{ class }} Queue Job.""" - -from masonite.queues import Queueable - - -class {{ class }}(Queueable): - """A {{ class }} Job.""" - - def __init__(self): - """A {{ class }} Constructor.""" - pass - - def handle(self): - """Logic to handle the job.""" - pass - diff --git a/src/masonite/snippets/scaffold/mailable.html b/src/masonite/snippets/scaffold/mailable.html deleted file mode 100644 index 2bf5fb3f7..000000000 --- a/src/masonite/snippets/scaffold/mailable.html +++ /dev/null @@ -1,21 +0,0 @@ -"""A {{ class }} Mailable.""" - -from masonite.drivers import Mailable - - -class {{ class }}(Mailable): - """A {{ class }} Mailable.""" - - def __init__(self, to): - """A {{ class }} Initializer.""" - self._to = to - - def build(self): - """Logic to handle the job.""" - return ( - self.subject('Subject Line') - .send_from('admin@example.com') - .reply_to('service@example.com') - .to(self._to) - # .view('template') - ) diff --git a/src/masonite/snippets/scaffold/middleware.html b/src/masonite/snippets/scaffold/middleware.html deleted file mode 100644 index a76832962..000000000 --- a/src/masonite/snippets/scaffold/middleware.html +++ /dev/null @@ -1,24 +0,0 @@ -"""{{ class }} Middleware.""" - -from masonite.request import Request - - -class {{ class }}Middleware: - """{{ class }} Middleware.""" - - def __init__(self, request: Request): - """Inject Any Dependencies From The Service Container. - - Arguments: - Request {masonite.request.Request} -- The Masonite request object - """ - self.request = request - - def before(self): - """Run This Middleware Before The Route Executes.""" - pass - - def after(self): - """Run This Middleware After The Route Executes.""" - pass - diff --git a/src/masonite/snippets/scaffold/model.html b/src/masonite/snippets/scaffold/model.html deleted file mode 100644 index 260745b53..000000000 --- a/src/masonite/snippets/scaffold/model.html +++ /dev/null @@ -1,8 +0,0 @@ -"""{{ class }} Model.""" - -from masoniteorm.models import Model - - -class {{ class }}(Model): - """{{ class }} Model.""" - pass diff --git a/src/masonite/snippets/scaffold/provider.html b/src/masonite/snippets/scaffold/provider.html deleted file mode 100644 index 0f82fe695..000000000 --- a/src/masonite/snippets/scaffold/provider.html +++ /dev/null @@ -1,18 +0,0 @@ -"""A {{ class }} Service Provider.""" - -from masonite.provider import ServiceProvider - - -class {{ class }}(ServiceProvider): - """Provides Services To The Service Container.""" - - wsgi = False - - def register(self): - """Register objects into the Service Container.""" - pass - - def boot(self): - """Boots services required by the container.""" - pass - diff --git a/src/masonite/snippets/scaffold/test.html b/src/masonite/snippets/scaffold/test.html deleted file mode 100644 index f192d0950..000000000 --- a/src/masonite/snippets/scaffold/test.html +++ /dev/null @@ -1,23 +0,0 @@ -"""{{ class }} Testcase.""" - -from masonite.testing import TestCase - - -class {{ class }}(TestCase): - - """All tests by default will run inside of a database transaction.""" - transactions = True - - def setUp(self): - """Anytime you override the setUp method you must call the setUp method - on the parent class like below. - """ - super().setUp() - - def setUpFactories(self): - """This runs when the test class first starts up. - This does not run before every test case. Use this method to - set your database up. - """ - pass - diff --git a/src/masonite/snippets/scaffold/validator.html b/src/masonite/snippets/scaffold/validator.html deleted file mode 100644 index d9b1bcdc6..000000000 --- a/src/masonite/snippets/scaffold/validator.html +++ /dev/null @@ -1,8 +0,0 @@ -"""A {{ class }} Validator""" - -from masonite.validator import Validator - - -class {{ class }}(Validator): - pass - diff --git a/src/masonite/snippets/statuscode.html b/src/masonite/snippets/statuscode.html deleted file mode 100644 index 8d14b6127..000000000 --- a/src/masonite/snippets/statuscode.html +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - -- - {% if type(getattr(obj, key)) in show_methods %} - - {% if key.startswith('__') %} - - - {% elif key.startswith('_') %} - # - {% else %} - + - {% endif %} - {{ key }} - - - {% if not hasattr(property, '__self__') %} - {{ type(getattr(obj, key)).__name__ }} - {% if type(getattr(obj, key)) == dict %} - : {{ len(getattr(obj, key)) }} - {% if len(getattr(obj, key)) %} -- -
- {% for key, value in getattr(obj, key).items() %} -{{ key }}: {{ value }}- {% endfor %} - {% endif %} - {% elif type(getattr(obj, key)) == list %} - : {{ len(getattr(obj, key)) }} - {% if len(getattr(obj, key)) %} -
- {% for key in getattr(obj, key) %} - {% if isinstance(key, Model) %} -{{ key }}- {% endif %} - {% endfor %} - {% endif %} - {% else %} - {{ property }} - {% endif %} - {% endif %} - {% endif %} - -{{ code }} - - - - - - - - - -- -- - - \ No newline at end of file diff --git a/src/masonite/storage.py b/src/masonite/storage.py deleted file mode 100644 index bdea7fa63..000000000 --- a/src/masonite/storage.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Module for Storage class.""" - -import os - - -class Storage: - """Responsible for compiling Sass.""" - - def __init__(self): - """Storage constructor.""" - pass - - def compile_sass(self): - """Compile sass.""" - from config import application, storage - - try: - import sass - except ImportError: - pass - else: - matches = [] - for files in storage.SASSFILES["importFrom"]: - for root, _, filenames in os.walk( - os.path.join(application.BASE_DIRECTORY, files) - ): - for filename in filenames: - if filename.endswith( - (".sass", ".scss") - ) and not filename.startswith("_"): - matches.append(os.path.join(root, filename)) - - for filename in matches: - with open(filename) as f: - compiled_sass = sass.compile( - string=f.read(), include_paths=storage.SASSFILES["includePaths"] - ) - name = ( - filename.split(os.sep)[-1] - .replace(".scss", "") - .replace(".sass", "") - ) - write_file = os.path.join( - os.path.join( - application.BASE_DIRECTORY, storage.SASSFILES["compileTo"] - ), - "{0}.css".format(name), - ) - with open(write_file, "w") as r: - r.write(compiled_sass) diff --git a/src/masonite/storage/__init__.py b/src/masonite/storage/__init__.py new file mode 100644 index 000000000..abff98db6 --- /dev/null +++ b/src/masonite/storage/__init__.py @@ -0,0 +1 @@ +from .storage import StorageCapsule diff --git a/src/masonite/storage/storage.py b/src/masonite/storage/storage.py new file mode 100644 index 000000000..46acb4b21 --- /dev/null +++ b/src/masonite/storage/storage.py @@ -0,0 +1,10 @@ +class StorageCapsule: + def __init__(self): + self.storage_templates = {} + + def add_storage_assets(self, templates): + self.storage_templates.update(templates) + return self + + def get_storage_assets(self): + return self.storage_templates diff --git a/src/masonite/stubs/controllers/Controller.py b/src/masonite/stubs/controllers/Controller.py new file mode 100644 index 000000000..a42e6e35b --- /dev/null +++ b/src/masonite/stubs/controllers/Controller.py @@ -0,0 +1,7 @@ +from masonite.controllers import Controller +from masonite.views import View + + +class __class__(Controller): + def show(self, view: View): + return view.render("") diff --git a/src/masonite/stubs/controllers/auth/HomeController.py b/src/masonite/stubs/controllers/auth/HomeController.py new file mode 100644 index 000000000..c44ee0f1a --- /dev/null +++ b/src/masonite/stubs/controllers/auth/HomeController.py @@ -0,0 +1,7 @@ +from masonite.controllers import Controller +from masonite.views import View + + +class HomeController(Controller): + def show(self, view: View): + return view.render("auth.home") diff --git a/src/masonite/stubs/controllers/auth/LoginController.py b/src/masonite/stubs/controllers/auth/LoginController.py new file mode 100644 index 000000000..dcc25cb39 --- /dev/null +++ b/src/masonite/stubs/controllers/auth/LoginController.py @@ -0,0 +1,19 @@ +from masonite.controllers import Controller +from masonite.views import View +from masonite.request import Request +from masonite.response import Response +from masonite.authentication import Auth + + +class LoginController(Controller): + def show(self, view: View): + return view.render("auth.login") + + def store(self, view: View, request: Request, auth: Auth, response: Response): + login = auth.attempt(request.input("username"), request.input("password")) + + if login: + return response.redirect(name="home") + + # Go back to login page + return response.redirect(name="login") diff --git a/src/masonite/stubs/controllers/auth/PasswordResetController.py b/src/masonite/stubs/controllers/auth/PasswordResetController.py new file mode 100644 index 000000000..2d432d9c5 --- /dev/null +++ b/src/masonite/stubs/controllers/auth/PasswordResetController.py @@ -0,0 +1,28 @@ +from masonite.controllers import Controller +from masonite.views import View +from masonite.request import Request +from masonite.response import Response +from masonite.authentication import Auth + + +class PasswordResetController(Controller): + def show(self, view: View): # Show password_reset page + return view.render("auth.password_reset") + + def store( + self, auth: Auth, request: Request, response: Response + ): # store password_reset record + auth.password_reset(request.input("email")) + return "event fired" + + def change_password(self, view: View): # store password_reset record + return view.render("auth.change_password") + + def store_changed_password( + self, auth: Auth, request: Request, response: Response + ): # store password_reset record + auth.reset_password(request.input("password"), request.input("token")) + + # Need to validate?? + # Redirect back? + return response.back() diff --git a/src/masonite/stubs/controllers/auth/RegisterController.py b/src/masonite/stubs/controllers/auth/RegisterController.py new file mode 100644 index 000000000..1c2089af7 --- /dev/null +++ b/src/masonite/stubs/controllers/auth/RegisterController.py @@ -0,0 +1,20 @@ +from masonite.controllers import Controller +from masonite.views import View +from masonite.request import Request +from masonite.response import Response +from masonite.authentication import Auth + + +class RegisterController(Controller): + def show(self, view: View): # Show register page + return view.render("auth.register") + + def store( + self, auth: Auth, request: Request, response: Response + ): # store register user + user = auth.register(request.only("name", "email", "password")) + + if not user: + return response.redirect("/register") + + return response.redirect("/home") diff --git a/src/masonite/stubs/events/event.py b/src/masonite/stubs/events/event.py new file mode 100644 index 000000000..a6853fded --- /dev/null +++ b/src/masonite/stubs/events/event.py @@ -0,0 +1,2 @@ +class __class__: + pass diff --git a/src/masonite/stubs/events/listener.py b/src/masonite/stubs/events/listener.py new file mode 100644 index 000000000..00879d870 --- /dev/null +++ b/src/masonite/stubs/events/listener.py @@ -0,0 +1,3 @@ +class __class__: + def handle(self, event): + pass diff --git a/src/masonite/stubs/jobs/Job.py b/src/masonite/stubs/jobs/Job.py new file mode 100644 index 000000000..8845bc01b --- /dev/null +++ b/src/masonite/stubs/jobs/Job.py @@ -0,0 +1,6 @@ +from masonite.queues import Queueable + + +class __class__(Queueable): + def handle(self): + pass diff --git a/src/masonite/stubs/mailable/Mailable.py b/src/masonite/stubs/mailable/Mailable.py new file mode 100644 index 000000000..afe38eea9 --- /dev/null +++ b/src/masonite/stubs/mailable/Mailable.py @@ -0,0 +1,12 @@ +from masonite.mail import Mailable + + +class __class__(Mailable): + def build(self): + return ( + self.to("user@gmail.com") + .subject("Masonite 4") + .from_("admin@gmail.com") + .text("Hello from Masonite!") + .html("--- {{ code }} -- -Hello from Masonite!
") + ) diff --git a/src/masonite/stubs/notification/Notification.py b/src/masonite/stubs/notification/Notification.py new file mode 100644 index 000000000..b4d6330b9 --- /dev/null +++ b/src/masonite/stubs/notification/Notification.py @@ -0,0 +1,15 @@ +from masonite.notification import Notification +from masonite.mail import Mailable + + +class __class__(Notification, Mailable): + def to_mail(self, notifiable): + return ( + self.to(notifiable.email) + .subject("Masonite 4") + .from_("hello@email.com") + .text(f"Hello {notifiable.name}") + ) + + def via(self, notifiable): + return ["mail"] diff --git a/src/masonite/stubs/notification/create_notifications_table.py b/src/masonite/stubs/notification/create_notifications_table.py new file mode 100644 index 000000000..af83657ca --- /dev/null +++ b/src/masonite/stubs/notification/create_notifications_table.py @@ -0,0 +1,17 @@ +from masoniteorm.migrations import Migration + + +class CreateNotificationsTable(Migration): + def up(self): + """Run the migrations.""" + with self.schema.create("notifications") as table: + table.big_increments("id").primary() + table.string("type") + table.text("data") + table.morphs("notifiable") + table.datetime("read_at").nullable() + table.timestamps() + + def down(self): + """Revert the migrations.""" + self.schema.drop("notifications") diff --git a/src/masonite/stubs/policies/ModelPolicy.py b/src/masonite/stubs/policies/ModelPolicy.py new file mode 100644 index 000000000..681ea4835 --- /dev/null +++ b/src/masonite/stubs/policies/ModelPolicy.py @@ -0,0 +1,24 @@ +from masonite.authorization import Policy + + +class __class__(Policy): + def create(self, user): + return False + + def view_any(self, user): + return False + + def view(self, user, instance): + return False + + def update(self, user, instance): + return False + + def delete(self, user, instance): + return False + + def force_delete(self, user, instance): + return False + + def restore(self, user, instance): + return False diff --git a/src/masonite/stubs/policies/Policy.py b/src/masonite/stubs/policies/Policy.py new file mode 100644 index 000000000..5d5b29eb3 --- /dev/null +++ b/src/masonite/stubs/policies/Policy.py @@ -0,0 +1,6 @@ +from masonite.authorization import Policy + + +class __class__(Policy): + def view_admin(self, user): + return False diff --git a/src/masonite/stubs/providers/Provider.py b/src/masonite/stubs/providers/Provider.py new file mode 100644 index 000000000..16b7f9f23 --- /dev/null +++ b/src/masonite/stubs/providers/Provider.py @@ -0,0 +1,12 @@ +from masonite.providers import Provider + + +class __class__(Provider): + def __init__(self, application): + self.application = application + + def register(self): + pass + + def boot(self): + pass diff --git a/src/masonite/snippets/migrations/create_failed_jobs_table.py b/src/masonite/stubs/queue/create_failed_jobs_table.py similarity index 51% rename from src/masonite/snippets/migrations/create_failed_jobs_table.py rename to src/masonite/stubs/queue/create_failed_jobs_table.py index 1a9cd749e..ca039e888 100644 --- a/src/masonite/snippets/migrations/create_failed_jobs_table.py +++ b/src/masonite/stubs/queue/create_failed_jobs_table.py @@ -6,12 +6,14 @@ def up(self): """Run the migrations.""" with self.schema.create("failed_jobs") as table: table.increments("id") - table.string("queue") - table.string("driver") - table.string("channel") + table.string("queue").nullable() + table.string("connection").nullable() + table.string("name").nullable() + table.string("driver").nullable() table.binary("payload") - table.timestamp("failed_at") - table.timestamps() + table.text("exception").nullable() + table.timestamp("failed_at").nullable() + table.timestamp("created_at").nullable() def down(self): """Revert the migrations.""" diff --git a/src/masonite/snippets/migrations/create_queue_jobs_table.py b/src/masonite/stubs/queue/create_queue_jobs_table.py similarity index 60% rename from src/masonite/snippets/migrations/create_queue_jobs_table.py rename to src/masonite/stubs/queue/create_queue_jobs_table.py index 3ee3e3102..f4c27b11d 100644 --- a/src/masonite/snippets/migrations/create_queue_jobs_table.py +++ b/src/masonite/stubs/queue/create_queue_jobs_table.py @@ -4,18 +4,17 @@ class CreateQueueJobsTable(Migration): def up(self): """Run the migrations.""" - with self.schema.create("queue_jobs") as table: + with self.schema.create("jobs") as table: table.increments("id") - table.string("queue") table.string("name") - table.binary("serialized") + table.string("queue") + table.binary("payload") table.integer("attempts") - table.integer("failed").nullable() table.timestamp("ran_at").nullable() - table.timestamp("created_at").nullable() - table.timestamp("available_at").nullable() + table.timestamp("available_at", now=True).nullable() table.timestamp("reserved_at").nullable() + table.timestamp("created_at", now=True).nullable() def down(self): """Revert the migrations.""" - self.schema.drop("queue_jobs") + self.schema.drop("jobs") diff --git a/src/masonite/stubs/scheduling/task.py b/src/masonite/stubs/scheduling/task.py new file mode 100644 index 000000000..f523ae31c --- /dev/null +++ b/src/masonite/stubs/scheduling/task.py @@ -0,0 +1,7 @@ +"""Task Module Description""" +from masonite.scheduling import Task + + +class __class__(Task): + def handle(self): + pass diff --git a/src/masonite/stubs/templates/auth/base.html b/src/masonite/stubs/templates/auth/base.html new file mode 100644 index 000000000..015d2dee8 --- /dev/null +++ b/src/masonite/stubs/templates/auth/base.html @@ -0,0 +1,13 @@ + + + + + + +Masonite 4 + + + + {% block content %}{% endblock %} + + \ No newline at end of file diff --git a/src/masonite/stubs/templates/auth/change_password.html b/src/masonite/stubs/templates/auth/change_password.html new file mode 100644 index 000000000..158fd756b --- /dev/null +++ b/src/masonite/stubs/templates/auth/change_password.html @@ -0,0 +1,49 @@ + +{% extends 'auth/base.html' %} + +{% block content %} ++ ++ +{% endblock %} \ No newline at end of file diff --git a/src/masonite/stubs/templates/auth/home.html b/src/masonite/stubs/templates/auth/home.html new file mode 100644 index 000000000..51c6eef7b --- /dev/null +++ b/src/masonite/stubs/templates/auth/home.html @@ -0,0 +1,17 @@ + +{% extends 'auth/base.html' %} + +{% block content %} ++ +++ + +++ Password Reset Request +
+ + ++ ++ +{% endblock %} \ No newline at end of file diff --git a/src/masonite/stubs/templates/auth/login.html b/src/masonite/stubs/templates/auth/login.html new file mode 100644 index 000000000..fd22091f4 --- /dev/null +++ b/src/masonite/stubs/templates/auth/login.html @@ -0,0 +1,66 @@ + +{% extends 'auth/base.html' %} + +{% block content %} ++ +++ Welcome! +++ ++ +{% endblock %} \ No newline at end of file diff --git a/src/masonite/stubs/templates/auth/password_reset.html b/src/masonite/stubs/templates/auth/password_reset.html new file mode 100644 index 000000000..9de6964be --- /dev/null +++ b/src/masonite/stubs/templates/auth/password_reset.html @@ -0,0 +1,40 @@ + +{% extends 'auth/base.html' %} + +{% block content %} ++ +++ + +++ Login +
+ + ++ ++ +{% endblock %} \ No newline at end of file diff --git a/src/masonite/stubs/templates/auth/register.html b/src/masonite/stubs/templates/auth/register.html new file mode 100644 index 000000000..a03f122eb --- /dev/null +++ b/src/masonite/stubs/templates/auth/register.html @@ -0,0 +1,72 @@ + +{% extends 'auth/base.html' %} + +{% block content %} ++ +++ + +++ Password Reset Request +
+ + ++ ++ +{% endblock %} \ No newline at end of file diff --git a/src/masonite/stubs/validation/Rule.py b/src/masonite/stubs/validation/Rule.py new file mode 100644 index 000000000..4838dbbb9 --- /dev/null +++ b/src/masonite/stubs/validation/Rule.py @@ -0,0 +1,48 @@ +"""__class__ validation""" + +from masonite.validation import BaseValidation + + +class __class__(BaseValidation): + """__class__ validation class""" + + def passes(self, attribute, key, dictionary): + """The passing criteria for this rule. + + This should return a True boolean value. + + Arguments: + attribute {mixed} -- The value found within the dictionary + key {string} -- The key in the dictionary being searched for. + This key may or may not exist in the dictionary. + dictionary {dict} -- The dictionary being searched + + Returns: + bool + """ + return attribute + + def message(self, key): + """A message to show when this rule fails + + Arguments: + key {string} -- The key used to search the dictionary + + Returns: + string + """ + return f"{key} is required" + + def negated_message(self, key): + """A message to show when this rule is negated using a negation rule like 'isnt()' + + For example if you have a message that says 'this is required' you may have a negated statement + that says 'this is not required'. + + Arguments: + key {string} -- The key used to search the dictionary + + Returns: + string + """ + return "{key} is not required" diff --git a/src/masonite/stubs/validation/RuleEnclosure.py b/src/masonite/stubs/validation/RuleEnclosure.py new file mode 100644 index 000000000..8d4adaf95 --- /dev/null +++ b/src/masonite/stubs/validation/RuleEnclosure.py @@ -0,0 +1,18 @@ +""" __class__ Validation Enclosure """ + +from masonite.validation import RuleEnclosure + + +class __class__(RuleEnclosure): + """__class__ Validation Enclosure Class.""" + + def rules(self): + """Used to return a list of rules in order to make validation + more reusable. + + Returns: + list -- List of rules + """ + return [ + # Rules go here + ] diff --git a/src/masonite/snippets/__init__.py b/src/masonite/templates/__init__.py similarity index 100% rename from src/masonite/snippets/__init__.py rename to src/masonite/templates/__init__.py diff --git a/src/masonite/snippets/exceptions/dump.html b/src/masonite/templates/dump.html similarity index 100% rename from src/masonite/snippets/exceptions/dump.html rename to src/masonite/templates/dump.html diff --git a/src/masonite/templates/obj_loop.html b/src/masonite/templates/obj_loop.html new file mode 100644 index 000000000..1bd5d3141 --- /dev/null +++ b/src/masonite/templates/obj_loop.html @@ -0,0 +1,43 @@ ++ +++ + +++ New Account +
+ + ++\ No newline at end of file diff --git a/src/masonite/testing/BaseRequest.py b/src/masonite/testing/BaseRequest.py deleted file mode 100644 index 61d6038dc..000000000 --- a/src/masonite/testing/BaseRequest.py +++ /dev/null @@ -1,18 +0,0 @@ -class BaseRequest: - def user(self, obj): - self._user = obj - self.container.on_resolve("Request", self._bind_user_to_request) - wsgi = generate_wsgi() - wsgi["PATH_INFO"] = self.url - self._run_container(wsgi) - - return self - - def ok(self): - return self.status("200 OK") - - def status(self, value=None): - if not value: - return self.container.make("Request").get_status_code() - - return self.container.make("Request").get_status_code() == value diff --git a/src/masonite/testing/MockRoute.py b/src/masonite/testing/MockRoute.py deleted file mode 100644 index ffacf7eb9..000000000 --- a/src/masonite/testing/MockRoute.py +++ /dev/null @@ -1,381 +0,0 @@ -import json - -from ..view import View -from ..helpers import Dot -from ..request import Request -from ..response import Response - - -class MockRoute: - def __init__(self, route, container, wsgi=None): - self.route = route - self.container = container - self.wsgi = wsgi - - def assertIsNamed(self, name): - assert self.route.named_route == name, "Route name is {}. Asserted {}".format( - self.route.named_route, name - ) - return self - - def assertIsNotNamed(self): - assert self.route.named_route is None, "Route has a name: {}".format( - self.route.named_route - ) - return self - - def isNamed(self, name): - return self.route.named_route == name - - def hasMiddleware(self, *middleware): - return all(elem in self.route.list_middleware for elem in middleware) - - def hasController(self, controller): - return self.route.controller == controller - - def ensure_argument_is_controller_name(self, controller_name): - return isinstance(controller_name, str) and "@" in controller_name - - def assertHasController(self, controller): - if self.ensure_argument_is_controller_name(controller): - controller, method = controller.split("@") - assert ( - self.route.controller.__name__ == controller - ), "Controller is {}. Asserted {}".format( - self.route.controller.__name__, controller - ) - assert ( - self.route.controller_method == method - ), "Controller method is {}. Asserted {}".format( - self.route.controller_method, method - ) - - return self - - def contains(self, value): - return value in self.container.make(Response).content.decode("utf-8") - - def assertContains(self, value): - assert self.contains(value), "Response does not contain {}".format(value) - return self - - def assertNotFound(self): - return self.assertIsStatus(404) - - def ok(self): - return "200 OK" in self.container.make(Response).get_status_code() - - def canView(self): - return self.ok() - - def get_string_response(self): - response = self.container.make(Response).content - - if isinstance(response, str): - return response - - return response.decode("utf-8") - - def hasJson(self, key, value=""): - - response = json.loads(self.get_string_response()) - if isinstance(key, dict): - for item_key, key_value in key.items(): - if not Dot().dot(item_key, response, False) == key_value: - return False - return True - return Dot().dot(key, response, False) - - def assertHasJson(self, key, value): - response = json.loads(self.get_string_response()) - if isinstance(key, dict): - for item_key, key_value in key.items(): - assert Dot().dot(item_key, response, False) == key_value - else: - assert ( - Dot().dot(key, response, False) == value - ), "Key '{}' with the value of '{}' could not find a match in {}".format( - key, value, response - ) - return self - - def assertJsonContains(self, key, value): - response = json.loads(self.get_string_response()) - if not isinstance(response, list): - raise ValueError( - "This method can only be used if the response is a list of elements." - ) - - found = False - for element in response: - if Dot().dot(key, element, False): - assert Dot().dot(key, element, False) - found = True - - if not found: - raise AssertionError( - "Could not find a key of: {} that had the value of {}".format( - key, value - ) - ) - return self - - def count(self, amount): - return len(json.loads(self.get_string_response())) == amount - - def assertCount(self, amount): - response_amount = len(json.loads(self.get_string_response())) - assert ( - response_amount == amount - ), "Response has an count of {}. Asserted {}".format(response_amount, amount) - return self - - def amount(self, amount): - return self.count(amount) - - def hasAmount(self, key, amount): - response = json.loads(self.get_string_response()) - try: - return len(response[key]) == amount - except TypeError: - raise TypeError( - "The json response key of: {} is not iterable but has the value of {}".format( - key, response[key] - ) - ) - - def assertHasAmount(self, key, amount): - response = json.loads(self.get_string_response()) - try: - assert len(response[key]) == amount, "{} is not equal to {}".format( - len(response[key]), amount - ) - except TypeError: - raise TypeError( - "The json response key of: {} is not iterable but has the value of {}".format( - key, response[key] - ) - ) - - return self - - def assertNotHasAmount(self, key, amount): - response = json.loads(self.get_string_response()) - try: - assert ( - not len(response[key]) == amount - ), "{} is equal to {} but should not be".format(len(response[key]), amount) - except TypeError: - raise TypeError( - "The json response key of: {} is not iterable but has the value of {}".format( - key, response[key] - ) - ) - - return self - - def user(self, obj): - self._user = obj - self.container.on_resolve(Request, self._bind_user_to_request) - return self - - def isPost(self): - return "POST" in self.route.method_type - - def isGet(self): - return "GET" in self.route.method_type - - def isPut(self): - return "PUT" in self.route.method_type - - def isPatch(self): - return "PATCH" in self.route.method_type - - def isDelete(self): - return "DELETE" in self.route.method_type - - def on_bind(self, obj, method): - self.container.on_bind(obj, method) - return self - - def hasSession(self, key): - return self.container.make("Session").has(key) - - def assertParameterIs(self, key, value): - request = self.container.make("Request") - if key not in request.url_params: - raise AssertionError( - "Request class does not have the '{}' url parameter".format(key) - ) - - if request.param(key) != value: - raise AssertionError( - "parameter {} is equal to {} of type {}, not {} of type {}".format( - key, - request.param(key), - type(request.param(key)), - value, - type(value), - ) - ) - - def assertIsStatus(self, status): - response = self.container.make(Response) - assert response.is_status(status), AssertionError( - "{} is not equal to {}".format(response.get_status_code(), status) - ) - if not response.is_status(status): - raise AssertionError( - "{} is not equal to {}".format(response.get_status_code(), status) - ) - - return self - - def assertHasHeader(self, key): - response = self.container.make(Response) - assert response.header(key), "Header '{}' does not exist".format(key) - return self - - def assertNotHasHeader(self, key): - request = self.container.make("Request") - assert not request.header( - key - ), "Header '{}' exists but asserting it should not".format(key) - return self - - def assertHeaderIs(self, key, value): - response = self.container.make(Response) - - header = response.header(key) - if not header: - raise ValueError(f"Header {key} is not set") - if header: - header = header.value - - assert header == str(value), AssertionError( - "{} is not equal to {}".format(header, value) - ) - - return self - - def assertPathIs(self, url): - path = self.container.make("Request").path - assert path == url, "Asserting the path is '{}' but it is '{}'".format( - url, path - ) - return True - - def session(self, key): - return self.container.make("Session").get(key) - - def on_make(self, obj, method): - self.container.on_make(obj, method) - return self - - def on_resolve(self, obj, method): - self.container.on_resolve(obj, method) - return self - - def _bind_user_to_request(self, request, container): - request.set_user(self._user) - return self - - def headerIs(self, key, value): - response = self.container.make(Response) - header = response.header(key) - if not header: - raise AssertionError(f"Could not found the {header} header") - assertion = header.value == value - if not assertion: - raise AssertionError( - "header {} does not equal {}".format(response.header(key), value) - ) - return assertion - - def parameterIs(self, key, value): - request = self.container.make("Request") - assertion = request.param(key) == value - if not assertion: - raise AssertionError( - "parameter {} is equal to {} of type {}, not {} of type {}".format( - key, - request.param(key), - type(request.param(key)), - value, - type(value), - ) - ) - return assertion - - @property - def request(self): - return self.container.make("Request") - - @property - def response(self): - """Gets the string response from the container. This isinstance check here - is to support Python 3.5. Once python3.5 goes away we can can remove this check. - - @required for 3.5 - - Returns: - string - """ - response = self.get_string_response() - if isinstance(response, str): - return response - - return response.decode("utf-8") - - def asDictionary(self): - try: - return json.loads(self.response) - except ValueError: - raise ValueError("The response was not json serializable") - - def ensure_response_has_view(self): - """Ensure that the response has a view as its original content.""" - if not self.response_has_view(): - raise ValueError("The response is not a view") - - def response_has_view(self): - return self.route.original and isinstance(self.route.original, View) - - def assertViewIs(self, name): - """Assert that request renders the given view name.""" - self.ensure_response_has_view() - assert self.route.original.template == name - return self - - def assertViewHas(self, key, value=None): - """Assert that view context contains a given data key (and eventually associated value).""" - self.ensure_response_has_view() - assert key in self.route.original.dictionary - if value: - assert self.route.original.dictionary[key] == value - - def assertViewHasAll(self, keys): - """Assert that view context contains exactly the data keys (or the complete data dict).""" - self.ensure_response_has_view() - if isinstance(keys, list): - assert set(keys) == set(self.route.original.dictionary.keys()) - set( - self.route.original._shared.keys() - ) - else: - view_data = self.route.original.dictionary - for key in self.route.original._shared: - del view_data[key] - assert keys == view_data - - def assertViewMissing(self, key): - """Assert that given data key is not in the view context.""" - self.ensure_response_has_view() - assert key not in self.route.original.dictionary - - def assertRedirect(self, redirect_uri): - """Assert that response is redirection to the given view name or URI or Controller@method.""" - request = self.container.make("Request") - response = self.container.make(Response) - self.route.get_response() - assert response.is_status(302) or response.is_status(301) - assert request.redirect_url == redirect_uri diff --git a/src/masonite/testing/TestCase.py b/src/masonite/testing/TestCase.py deleted file mode 100644 index 621a5ab6c..000000000 --- a/src/masonite/testing/TestCase.py +++ /dev/null @@ -1,325 +0,0 @@ -import io -import json -import sys -import unittest -from contextlib import contextmanager -from urllib.parse import urlencode - -from .. import env -from ..exceptions import RouteNotFoundException -from ..helpers.migrations import Migrations -from ..helpers.routes import create_matchurl, flatten_routes -from .generate_wsgi import generate_wsgi -from .create_container import create_container -from masoniteorm.factories import Factory -from ..response import Response - -from .MockRoute import MockRoute -from ..helpers import config -from ..auth import Sign - - -class TestCase(unittest.TestCase): - - sqlite = True - transactions = True - refreshes_database = False - _transaction = False - - def setUp(self): - from wsgi import container - - self.container = container - self._with_subdomains = False - self.wsgi_overrides = {} - - self.acting_user = False - self.factory = Factory - self.withoutExceptionHandling() - self.withoutCsrf() - if not self._transaction: - self.startTransaction() - if hasattr(self, "setUpFactories"): - self.setUpFactories() - - if self.sqlite and env("DB_CONNECTION") != "sqlite": - raise Exception("Cannot run tests without using the 'sqlite' database.") - - if not self.transactions and self.refreshes_database: - self.refreshDatabase() - - self.route_middleware = {} - self.http_middleware = [] - self.use_http_middleware = True - self.headers = {} - - def buildOwnContainer(self): - self.container = self.create_container() - return self - - @classmethod - def setUpClass(cls): - cls.staticSetUpDatabase() - - @classmethod - def tearDownClass(cls): - if not cls.refreshes_database and cls.transactions: - cls.staticStopTransaction() - else: - cls.staticTearDownDatabase() - - def refreshDatabase(self): - if not self.refreshes_database and self.transactions: - self.stopTransaction() - self.startTransaction() - if hasattr(self, "setUpFactories"): - self.setUpFactories() - else: - self.tearDownDatabase() - self.setUpDatabase() - - def startTransaction(self): - from config.database import DB - - DB.begin_transaction() - self.__class__._transaction = True - - def stopTransaction(self): - from config.database import DB - - DB.rollback() - self.__class__._transaction = False - - def withWSGIOverride(self, wsgi_values={}): - self.wsgi_overrides = wsgi_values - return self - - @classmethod - def staticStopTransaction(cls): - from config.database import DB - - DB.rollback() - cls._transaction = False - - def make(self, model, factory, amount=50): - self.registerFactory(model, factory) - self.makeFactory(model, amount) - - def makeFactory(self, model, amount): - return self.factory(model, amount).create() - - def registerFactory(self, model, callable_factory): - self.factory.register(model, callable_factory) - - def setUpDatabase(self): - self.tearDownDatabase() - Migrations().run() - if hasattr(self, "setUpFactories"): - self.setUpFactories() - - def tearDownDatabase(self): - Migrations().reset() - - @staticmethod - def staticSetUpDatabase(): - Migrations().run() - - @staticmethod - def staticTearDownDatabase(): - Migrations().reset() - - def tearDown(self): - if not self.transactions and self.refreshes_database: - self.tearDownDatabase() - - if self.container.has("Request"): - self.container.make("Request").get_and_reset_headers() - - if self.container.has(Response): - self.container.make(Response).get_and_reset_headers() - - def call(self, method, url, params, wsgi={}): - custom_wsgi = {"PATH_INFO": url, "REQUEST_METHOD": method} - - custom_wsgi.update(wsgi) - if not self._with_csrf: - token = Sign().sign("secret") - params.update({"__token": token}) - custom_wsgi.update( - { - "HTTP_COOKIE": "csrf_token=" + token, - "CONTENT_LENGTH": len(str(json.dumps(params))), - "wsgi.input": io.BytesIO(bytes(json.dumps(params), "utf-8")), - } - ) - - custom_wsgi.update({"QUERY_STRING": urlencode(params)}) - self.run_container(custom_wsgi) - self.container.make("Request").request_variables = params - return self.route(url, method) - - def get(self, url, params={}, wsgi={}): - return self.call("GET", url, params, wsgi=wsgi) - - def withSubdomains(self): - self._with_subdomains = True - return self - - def json(self, method, url, params={}): - return self.call( - method, - url, - params, - wsgi={ - "CONTENT_TYPE": "application/json", - "CONTENT_LENGTH": len(str(json.dumps(params))), - "wsgi.input": io.BytesIO(bytes(json.dumps(params), "utf-8")), - }, - ) - - def post(self, url, params={}): - return self.call("POST", url, params) - - def put(self, url, params={}): - return self.json("PUT", url, params) - - def patch(self, url, params={}): - return self.json("PATCH", url, params) - - def delete(self, url, params={}): - return self.json("DELETE", url, params) - - def actingAs(self, user): - if not user: - raise TypeError("Cannot act as a user of type: {}".format(type(user))) - self.acting_user = user - return self - - def route(self, url, method=False): - for route in self.container.make("WebRoutes"): - matchurl = create_matchurl(url, route) - if self.container.make("Request").has_subdomain(): - route.load_request(self.container.make("Request")) - # Check if the subdomain matches the correct routes domain - if not route.has_required_domain(): - continue - - if matchurl.match(url) and method in route.method_type: - return MockRoute(route, self.container) - - raise RouteNotFoundException( - "Could not find a route based on the url '{}'".format(url) - ) - - def routes(self, routes=[], only=False): - if only: - self.container.bind("WebRoutes", flatten_routes(only)) - return - - self.container.bind( - "WebRoutes", flatten_routes(self.container.make("WebRoutes") + routes) - ) - - @contextmanager - def captureOutput(self): - new_out, new_err = io.StringIO(), io.StringIO() - old_out, old_err = sys.stdout, sys.stderr - try: - sys.stdout, sys.stderr = new_out, new_err - yield sys.stdout - finally: - sys.stdout, sys.stderr = old_out, old_err - - def run_container(self, wsgi_values={}): - wsgi = generate_wsgi() - wsgi.update(wsgi_values) - wsgi.update(self.wsgi_overrides) - self.container.bind("Environ", wsgi) - self.container.bind("User", self.acting_user) - - if self._with_subdomains: - self.container.bind("Subdomains", True) - - # if self.headers: - # self.container.make("Request").header(self.headers) - - if self.route_middleware: - self.container.bind("RouteMiddleware", self.route_middleware) - else: - self.container.bind( - "RouteMiddleware", config("middleware.route_middleware") - ) - - if self.use_http_middleware: - self.container.bind("HttpMiddleware", config("middleware.http_middleware")) - else: - self.container.bind("HttpMiddleware", self.http_middleware) - # self.container.bind("RouteMiddleware", config("middleware.route_middleware")) - - # if self.http_middleware is not False: - # self.container.bind("HttpMiddleware", self.http_middleware) - - try: - for provider in self.container.make("WSGIProviders"): - self.container.resolve(provider.boot) - self.container.make("Request")._test_user = self.acting_user - except Exception as e: # skipcq - if self._exception_handling: - self.container.make("ExceptionHandler").load_exception(e) - else: - raise e - - def withExceptionHandling(self): - self._exception_handling = True - - def withoutExceptionHandling(self): - self._exception_handling = False - - def withCsrf(self): - self._with_csrf = True - return self - - def withoutCsrf(self): - self._with_csrf = False - return self - - def assertDatabaseHas(self, schema, value): - from masoniteorm.query import QueryBuilder - - table = schema.split(".")[0] - column = schema.split(".")[1] - - self.assertTrue(QueryBuilder().table(table).where(column, value).first()) - - def assertDatabaseNotHas(self, schema, value): - from masoniteorm.query import QueryBuilder - - table = schema.split(".")[0] - column = schema.split(".")[1] - - self.assertFalse(QueryBuilder().table(table).where(column, value).first()) - - def on_bind(self, obj, method): - self.container.on_bind(obj, method) - return self - - def withRouteMiddleware(self, middleware): - self.route_middleware = middleware - return self - - def withHttpMiddleware(self, middleware): - self.use_http_middleware = False - self.http_middleware = middleware - return self - - def withHeaders(self, headers={}): - self.headers = headers - return self - - def withoutHttpMiddleware(self): - self.use_http_middleware = False - self.http_middleware = [] - return self - - def create_container(self): - return create_container() diff --git a/src/masonite/testing/__init__.py b/src/masonite/testing/__init__.py deleted file mode 100644 index e33b67d89..000000000 --- a/src/masonite/testing/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .TestCase import TestCase -from .MockRoute import MockRoute -from .generate_wsgi import generate_wsgi, MockWsgiInput -from .create_container import create_container diff --git a/src/masonite/testing/create_container.py b/src/masonite/testing/create_container.py deleted file mode 100644 index 811ccf3ca..000000000 --- a/src/masonite/testing/create_container.py +++ /dev/null @@ -1,43 +0,0 @@ -from ..app import App -import copy - - -def create_container(): - container = copy.deepcopy(App()) - from .generate_wsgi import generate_wsgi - from config import providers - - container.bind("WSGI", generate_wsgi()) - container.bind("Container", container) - - # container.bind('ProvidersConfig', providers) - container.bind("Providers", []) - container.bind("WSGIProviders", []) - - """Bind all service providers - Let's register everything into the Service Container. Once everything is - in the container we can run through all the boot methods. For reasons - some providers don't need to execute with every request and should - only run once when the server is started. Providers will be ran - once if the wsgi attribute on a provider is False. - """ - - for provider in providers.PROVIDERS: - located_provider = provider() - located_provider.load_app(container).register() - if located_provider.wsgi: - container.make("WSGIProviders").append(located_provider) - else: - container.make("Providers").append(located_provider) - - for provider in container.make("Providers"): - container.resolve(provider.boot) - - """Get the application from the container - Some providers may change the WSGI Server like wrapping the WSGI server - in a Whitenoise container for an example. Let's get a WSGI instance - from the container and pass it to the application variable. This - will allow WSGI servers to pick it up from the command line - """ - - return container diff --git a/src/masonite/testing/generate_wsgi.py b/src/masonite/testing/generate_wsgi.py deleted file mode 100644 index f2519e1fc..000000000 --- a/src/masonite/testing/generate_wsgi.py +++ /dev/null @@ -1,39 +0,0 @@ -import io - - -class MockWsgiInput: - def __init__(self, data): - self.data = data - - def read(self, _): - return self.data - - -def generate_wsgi(): - return { - "wsgi.version": (1, 0), - "wsgi.multithread": False, - "wsgi.multiprocess": True, - "wsgi.run_once": False, - "wsgi.input": io.BytesIO(), - "SERVER_SOFTWARE": "gunicorn/19.7.1", - "REQUEST_METHOD": "GET", - "QUERY_STRING": "application=Masonite", - "RAW_URI": "/", - "SERVER_PROTOCOL": "HTTP/1.1", - "HTTP_HOST": "127.0.0.1:8000", - "HTTP_ACCEPT": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "HTTP_UPGRADE_INSECURE_REQUESTS": "1", - "HTTP_COOKIE": "setcookie=value", - "HTTP_USER_AGENT": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/604.4.7 (KHTML, like Gecko) Version/11.0.2 Safari/604.4.7", - "HTTP_ACCEPT_LANGUAGE": "en-us", - "HTTP_ACCEPT_ENCODING": "gzip, deflate", - "HTTP_CONNECTION": "keep-alive", - "wsgi.url_scheme": "http", - "REMOTE_ADDR": "127.0.0.1", - "REMOTE_PORT": "62241", - "SERVER_NAME": "127.0.0.1", - "SERVER_PORT": "8000", - "PATH_INFO": "/", - "SCRIPT_NAME": "", - } diff --git a/src/masonite/tests/DatabaseTransactions.py b/src/masonite/tests/DatabaseTransactions.py new file mode 100644 index 000000000..296c208fc --- /dev/null +++ b/src/masonite/tests/DatabaseTransactions.py @@ -0,0 +1,8 @@ +class DatabaseTransactions: + def startTestRun(self): + self.application.make("resolver").begin_transaction(self.connection) + return self + + def stopTestRun(self): + self.application.make("resolver").rollback(self.connection) + return self diff --git a/src/masonite/tests/HttpTestResponse.py b/src/masonite/tests/HttpTestResponse.py new file mode 100644 index 000000000..853f19d5d --- /dev/null +++ b/src/masonite/tests/HttpTestResponse.py @@ -0,0 +1,323 @@ +import json +from ..views import View +from ..controllers import Controller +from ..utils.structures import data_get + + +class HttpTestResponse: + def __init__(self, application, request, response, route): + self.application = application + self.request = request + self.response = response + self.route = route + self.content = None + self.status = None + self.get_response() + + def get_response(self): + self.content = self.response.get_response_content() + return self + + def get_content(self): + """Take care of decoding content if bytes and returns str.""" + return ( + self.content.decode("utf-8") + if isinstance(self.content, bytes) + else str(self.content) + ) + + def assertContains(self, content): + assert ( + content in self.get_content() + ), f"{content} not found in {self.get_content()}" + return self + + def assertNotContains(self, content): + assert content not in self.get_content() + return self + + def assertContainsInOrder(self, *content): + response_content = self.get_content() + index = 0 + for content_string in content: + found_at_index = response_content.find(content_string, index) + assert found_at_index != -1 + index = found_at_index + len(content_string) + return self + + def assertIsNamed(self, name): + assert ( + self.route.get_name() == name + ), f"Route name is {self.route.get_name()}. Asserted {name}" + return self + + def assertIsNotNamed(self, name=None): + if name is None: + assert self.route.name is None, "Route has a name: {}".format( + self.route.name + ) + else: + assert ( + self.route.get_name() != name + ), f"Route name {self.route.get_name()} matches expected {name}" + return self + + def assertIsStatus(self, status): + assert self.response.is_status( + status + ), f"Status is {self.response.get_status_code()}. Asserted {status}" + return self + + def assertNotFound(self): + return self.assertIsStatus(404) + + def assertOk(self): + return self.assertIsStatus(200) + + def assertCreated(self): + return self.assertIsStatus(201) + + def assertSuccessful(self): + assert 200 <= self.response.get_status_code() < 300 + return self + + def assertNoContent(self, status=204): + assert not self.get_content() + return self.assertIsStatus(status) + + def assertUnauthorized(self): + return self.assertIsStatus(401) + + def assertForbidden(self): + return self.assertIsStatus(403) + + def assertHasHeader(self, name, value=None): + header_value = self.response.header(name) + assert header_value, f"Could not find the header {name}" + if value: + assert value == header_value, f"Header '{name}' does not equal {value}" + + def assertHeaderMissing(self, name): + assert not self.response.header(name) + + def assertLocation(self, location): + return self.assertHasHeader("Location", location) + + def assertRedirect(self, url=None, name=None, params={}): + # we could assert 301 or 302 code => what if user uses another status code in redirect() + # here we are sure + assert self.get_content() == "Redirecting ..." + if url: + self.assertLocation(url) + elif name: + url = self.response._get_url_from_route_name(name, params) + self.assertLocation(url) + return self + + def assertCookie(self, name, value=None): + assert self.request.cookie_jar.exists(name) + if value is not None: + assert self.request.cookie_jar.get(name).value == value + return self + + def assertPlainCookie(self, name): + assert self.request.cookie_jar.exists(name) + assert not self.request.cookie_jar.get(name).secure + return self + + def assertCookieExpired(self, name): + self.assertCookie(name) + assert self.request.cookie_jar.is_expired(name) + return self + + def assertCookieNotExpired(self, name): + return not self.assertCookieExpired(name) + + def assertCookieMissing(self, name): + assert not self.request.cookie_jar.exists(name) + return self + + def assertSessionHas(self, key, value=None): + """Assert that session contains the given key with the corresponding value if given. + The session driver can be specified if necessary.""" + session = self.request.session + assert session.has(key) + if value is not None: + assert session.get(key) == value + return self + + def assertSessionMissing(self, key): + """Assert that session does not contain the given key. The session driver can be specified + if necessary.""" + assert not self.request.session.get(key) + return self + + def assertSessionHasErrors(self, keys=[]): + """Assert that session contains errors for the given list of keys (meaning that each given key + exists in 'errors' dict in session.) If no keys are given this will assert that the + sessions has errors without checking specific keys.""" + session = self.request.session + assert session.has("errors") + if keys: + errors = session.get("errors") + for key in keys: + assert errors.get(key) + return self + + def assertSessionHasNoErrors(self, keys=[]): + """Assert that session does not have any errors (meaning that session does not contain an + 'errors' key or 'errors' key is empty. If a list of keys is given, this will check + that there are no errors for each of those keys.""" + session = self.request.session + if not keys: + assert not session.has("errors") + else: + errors = session.get("errors") + for key in keys: + assert not errors.get(key) + return self + + def _ensure_response_has_view(self): + """Ensure that the response has a view as its original content.""" + if not (self.response.original and isinstance(self.response.original, View)): + raise ValueError("The response is not a view") + + def assertViewIs(self, name): + """Assert that request renders the given view name.""" + self._ensure_response_has_view() + assert ( + self.response.original.template == name + ), f"Template {self.response.original.template} is not equal to {name}" + return self + + def assertViewHas(self, key, value=None): + """Assert that view context contains a given data key (and eventually associated value).""" + self._ensure_response_has_view() + value_at_key = data_get(self.response.original.dictionary, key) + assert value_at_key + if value: + assert value_at_key == value + return self + + def assertViewHasExact(self, keys): + """Assert that view context contains exactly the data keys (or the complete data dict).""" + self._ensure_response_has_view() + if isinstance(keys, list): + assert set(keys) == set(self.response.original.dictionary.keys()) - set( + self.response.original._shared.keys() + ) + else: + view_data = self.response.original.dictionary + for key in self.response.original._shared: + del view_data[key] + assert keys == view_data + return self + + def assertViewMissing(self, key): + """Assert that given data key is not in the view context.""" + self._ensure_response_has_view() + assert not data_get(self.response.original.dictionary, key) + return self + + def assertAuthenticated(self): + assert self.application.make("auth").guard("web").user() + return self + + def assertGuest(self): + assert not self.application.make("auth").guard("web").user() + return self + + def assertAuthenticatedAs(self, user): + user = self.application.make("auth").guard("web").user() + assert user == user + return self + + def assertHasHttpMiddleware(self, middleware): + """Assert that the request/response cycle has the given middleware. The HTTP middleware + class should be given.""" + assert middleware in self.application.make("middleware").http_middleware + return self + + def assertHasRouteMiddleware(self, middleware): + """Assert that the route has the given middleware. The registration key of the middleware + should be used.""" + assert middleware in self.application.make("middleware").route_middleware + return self + + def assertHasController(self, controller): + """Assert that route used the given controller. The controller can be a class or + a string. If it's a string it should be formatted as follow: ControllerName@method""" + if isinstance(controller, str) and "@" in controller: + assert self.route.controller == controller + elif issubclass(controller, Controller): + assert self.route.controller_class == controller + else: + raise ValueError( + "controller must be a string like YourController@index or a Controller class" + ) + return self + + def assertRouteHasParameter(self, key, value=None): + assert key in self.route.url_list, "Route does not contain parameter {key}." + if value is not None: + # TODO + # @josephmancuso not sure how to check if the route parameter has the given value + # 1. play with the compiled regex but not sure how to do it + # 2. see below, correct ? after testing it it's not correct, there are several issues with + # this. forgot all this + # real_url = self.route.url.replace(f"@{key}", str(value)) + # assert self.route.matches(real_url) + pass + return self + + def _ensure_response_is_json(self): + """Parse response back from JSON into a dict, if an error happens the response was not + a JSON string.""" + try: + return json.loads(self.response.content) + except ValueError: + raise ValueError("The response was not JSON serializable") + + def assertJson(self, data): + """Assert that response is JSON and contains the given data dictionary. The assertion will + pass even if it is not an exact match.""" + response_data = self._ensure_response_is_json() + assert data.items() <= response_data.items() + return self + + def assertJsonPath(self, path, value=None): + """Assert that response is JSON and contains the given path, with eventually the given + value if provided. The path is a dotted path.""" + response_data = self._ensure_response_is_json() + data_at_path = data_get(response_data, path) + + assert data_at_path == value, f"'{data_at_path}' does not equal {value}" + return self + + def assertJsonExact(self, data): + """Assert that response is JSON and is exactly the given data.""" + response_data = self._ensure_response_is_json() + assert response_data == data, f"'{response_data}' does not equal {data}" + return self + + def assertJsonCount(self, count, key=None): + """Assert that JSON response is JSON and has the given count of keys at root level + or at the given key.""" + response_data = self._ensure_response_is_json() + if key is not None: + response_data = response_data.get(key, {}) + + response_count = len(response_data.keys()) + assert ( + response_count == count + ), f"JSON response count is {response_count}. Asserted {count}." + return self + + def assertJsonMissing(self, path): + """Assert that JSON response is JSON and does not contain given path. + The path can be a dotted path""" + response_data = self._ensure_response_is_json() + assert not data_get( + response_data, path + ), f"'{response_data}' is not missing from {data_get(response_data, path)}" + return self diff --git a/src/masonite/tests/MockInput.py b/src/masonite/tests/MockInput.py new file mode 100644 index 000000000..b7eb61a3a --- /dev/null +++ b/src/masonite/tests/MockInput.py @@ -0,0 +1,6 @@ +class MockInput: + def __init__(self, data): + self.data = data + + def read(self, _): + return self.data diff --git a/src/masonite/tests/TestCase.py b/src/masonite/tests/TestCase.py new file mode 100644 index 000000000..012886e99 --- /dev/null +++ b/src/masonite/tests/TestCase.py @@ -0,0 +1,251 @@ +import json +import io +import unittest +import pendulum + + +from ..routes import Route +from ..foundation.response_handler import testcase_handler +from ..utils.http import generate_wsgi +from ..request import Request +from ..response import Response +from ..environment import LoadEnvironment +from ..providers.RouteProvider import RouteProvider +from .TestCommand import TestCommand + + +class TestCase(unittest.TestCase): + def setUp(self): + LoadEnvironment("testing") + from wsgi import application + + self.application = application + self.original_class_mocks = {} + self._test_cookies = {} + self._test_headers = {} + if hasattr(self, "startTestRun"): + self.startTestRun() + self.withoutCsrf() + self._exception_handling = False + # boot providers as they won't not be loaded if the test is not doing a request + self.application.bind("environ", generate_wsgi()) + try: + for provider in self.application.get_providers(): + # if no request is made we don't need RouteProvider, and we can't load it anyway + # because we don't have created a CSRF token yet + if not isinstance(provider, RouteProvider): + application.resolve(provider.boot) + except Exception as e: + if not self._exception_handling: + raise e + self.application.make("exception_handler").handle(e) + + def tearDown(self): + # be sure to reset this between each test + self._exception_handling = False + if hasattr(self, "stopTestRun"): + self.stopTestRun() + + def withExceptionsHandling(self): + """Enable for the duration of a test the handling of exception through the exception + handler.""" + self._exception_handling = True + + def setRoutes(self, *routes): + self.application.make("router").set(Route.group(*routes, middleware=["web"])) + return self + + def addRoutes(self, *routes): + self.application.make("router").add(Route.group(*routes, middleware=["web"])) + return self + + def withCsrf(self): + self._csrf = True + return self + + def withoutCsrf(self): + self._csrf = False + return self + + def get(self, route, data=None): + return self.fetch(route, data, method="GET") + + def post(self, route, data=None): + return self.fetch(route, data, method="POST") + + def put(self, route, data=None): + return self.fetch(route, data, method="PUT") + + def patch(self, route, data=None): + return self.fetch(route, data, method="PATCH") + + def make_request( + self, data={}, path="/", query_string="application=Masonite", method="GET" + ): + request = Request(generate_wsgi(data, path, query_string, method)) + request.app = self.application + + self.application.bind("request", request) + return request + + def make_response(self, data={}): + request = Response(generate_wsgi(data)) + request.app = self.application + + self.application.bind("response", request) + return request + + def withExceptionsHandling(self): + self._exception_handling = True + + def fetch(self, route, data=None, method=None): + if data is None: + data = {} + if not self._csrf: + token = self.application.make("sign").sign("cookie") + data.update({"__token": "cookie"}) + wsgi_request = generate_wsgi( + { + "HTTP_COOKIE": f"SESSID={token}; csrf_token={token}", + "CONTENT_LENGTH": len(str(json.dumps(data))), + "REQUEST_METHOD": method, + "PATH_INFO": route, + "wsgi.input": io.BytesIO(bytes(json.dumps(data), "utf-8")), + } + ) + else: + wsgi_request = generate_wsgi( + { + "CONTENT_LENGTH": len(str(json.dumps(data))), + "REQUEST_METHOD": method, + "PATH_INFO": route, + "wsgi.input": io.BytesIO(bytes(json.dumps(data), "utf-8")), + } + ) + + request, response = testcase_handler( + self.application, + wsgi_request, + self.mock_start_response, + exception_handling=self._exception_handling, + ) + # add eventual cookies added inside the test (not encrypted to be able to assert value ?) + for name, value in self._test_cookies.items(): + request.cookie(name, value) + # add eventual headers added inside the test + for name, value in self._test_headers.items(): + request.header(name, value) + + route = self.application.make("router").find(route, method) + if route: + return self.application.make("tests.response").build( + self.application, request, response, route + ) + raise Exception(f"No route found for {route}") + + def mock_start_response(self, *args, **kwargs): + pass + + def craft(self, command, arguments_str=""): + """Run a given command in tests and obtain a TestCommand instance to assert command + outputs. + self.craft("controller", "Welcome").assertSuccess() + """ + return TestCommand(self.application).run(command, arguments_str) + + def fake(self, binding): + """Mock a service with its mocked implementation or with a given custom + one.""" + + # save original first + self.original_class_mocks.update( + {binding: self.application.make(binding, self.application)} + ) + # mock by overriding with mocked version + mock = self.application.make(f"mock.{binding}", self.application) + self.application.bind(binding, mock) + return mock + + def withCookies(self, cookies_dict): + self._test_cookies = cookies_dict + return self + + def withHeaders(self, headers_dict): + self._test_headers = headers_dict + return self + + def actingAs(self, user): + self.make_request() + self.application.make("auth").guard("web").login_by_id( + user.get_primary_key_value() + ) + + def restore(self, binding): + """Restore the service previously mocked to the original one.""" + original = self.original_class_mocks.get(binding) + self.application.bind(binding, original) + + def fakeTime(self, pendulum_datetime): + """Set a given pendulum instance to be returned when a "now" (or "today", "tomorrow", + "yesterday") instance is created. It's really useful during tests to check + timestamps logic.""" + pendulum.set_test_now(pendulum_datetime) + + def fakeTimeTomorrow(self): + """Set the mocked time as tomorrow.""" + self.fakeTime(pendulum.tomorrow()) + + def fakeTimeYesterday(self): + """Set the mocked time as yesterday.""" + self.fakeTime(pendulum.yesterday()) + + def fakeTimeInFuture(self, offset, unit="days"): + """Set the mocked time as an offset of days in the future. Unit can be specified + among pendulum units: seconds, minutes, hours, days, weeks, months, years.""" + self.restoreTime() + datetime = pendulum.now().add(**{unit: offset}) + self.fakeTime(datetime) + + def fakeTimeInPast(self, offset, unit="days"): + """Set the mocked time as an offset of days in the past. Unit can be specified + among pendulum units: seconds, minutes, hours, days, weeks, months, years.""" + self.restoreTime() + datetime = pendulum.now().subtract(**{unit: offset}) + self.fakeTime(datetime) + + def restoreTime(self): + """Restore time to correct one, so that pendulum new "now" instance are corrects. + This method will be typically called in tearDown() method of a test class.""" + # this will clear the mock + pendulum.set_test_now() + + def assertDatabaseCount(self, table, count): + self.assertEqual(self.application.make("builder").table(table).count(), count) + + def assertDatabaseHas(self, table, query_dict): + self.assertGreaterEqual( + self.application.make("builder").table(table).where(query_dict).count(), 1 + ) + + def assertDatabaseMissing(self, table, query_dict): + self.assertEqual( + self.application.make("builder").table(table).where(query_dict).count(), 0 + ) + + def assertDeleted(self, instance): + self.assertFalse( + self.application.make("builder") + .table(instance.get_table_name()) + .where(instance.get_primary_key(), instance.get_primary_key_value()) + .get() + ) + + def assertSoftDeleted(self, instance): + deleted_at_column = instance.get_deleted_at_column() + self.assertTrue( + self.application.make("builder") + .table(instance.get_table_name()) + .where(instance.get_primary_key(), instance.get_primary_key_value()) + .where_not_null(deleted_at_column) + .get() + ) diff --git a/src/masonite/tests/TestCommand.py b/src/masonite/tests/TestCommand.py new file mode 100644 index 000000000..f82df9c2b --- /dev/null +++ b/src/masonite/tests/TestCommand.py @@ -0,0 +1,58 @@ +from cleo import CommandTester + + +class TestCommand: + """This class allows us to test craft commands and asserts command outputs.""" + + def __init__(self, application): + self.application = application + + def run(self, command, arguments_str=""): + command = self.application.make("commands").command_application.find(command) + self.command_tester = CommandTester(command) + self.command_tester.execute(arguments_str) + return self + + def assertExactOutput(self, ref_output): + """Assert command output to be exactly the same as the given reference output.""" + output = self._get_output() + assert ref_output == output, f"Command output was: {output}, not {ref_output}" + return self + + def assertOutputContains(self, ref_output): + output = self._get_output() + assert ( + ref_output in output + ), f"Command output was: {output} and does not contain {ref_output}." + return self + + def assertOutputMissing(self, ref_output): + """Assert command output does not contain the given reference output.""" + output = self._get_output() + assert ( + ref_output not in output + ), f"Command output was: {output}, not {ref_output}" + return self + + def assertHasErrors(self): + assert self._get_errors() + return self + + def assertExactErrors(self, ref_errors): + errors = self._get_errors() + assert ( + errors == ref_errors + ), f"Command output has errors: {errors}, not {ref_errors}." + return self + + def assertSuccess(self): + """Assert that command returned a 0 exit code meaning that it ran successfully.""" + code = self.command_tester.status_code + assert 0 == code, "Command exited code is not 0: {code}." + return self + + def _get_errors(self): + return self.command_tester.io.fetch_error() + + def _get_output(self): + return self.command_tester.io.fetch_output() diff --git a/src/masonite/tests/TestResponseCapsule.py b/src/masonite/tests/TestResponseCapsule.py new file mode 100644 index 000000000..27cced8e8 --- /dev/null +++ b/src/masonite/tests/TestResponseCapsule.py @@ -0,0 +1,17 @@ +class TestResponseCapsule: + def __init__(self, base_test_response_class): + self.base_test_response_class = base_test_response_class + self.test_responses_classes = [] + + def add(self, *classes): + self.test_responses_classes.extend(classes) + return self + + def build(self, *args): + """Apply other test response class as mixins to base test response class.""" + obj = self.base_test_response_class(*args) + for cls in self.test_responses_classes: + base_cls = obj.__class__ + base_cls_name = obj.__class__.__name__ + obj.__class__ = type(base_cls_name, (base_cls, cls), {}) + return obj diff --git a/src/masonite/tests/__init__.py b/src/masonite/tests/__init__.py new file mode 100644 index 000000000..56f52af71 --- /dev/null +++ b/src/masonite/tests/__init__.py @@ -0,0 +1,4 @@ +from .TestCase import TestCase +from .MockInput import MockInput +from .HttpTestResponse import HttpTestResponse +from .DatabaseTransactions import DatabaseTransactions diff --git a/src/masonite/snippets/exceptions/__init__.py b/src/masonite/utils/__init__.py similarity index 100% rename from src/masonite/snippets/exceptions/__init__.py rename to src/masonite/utils/__init__.py diff --git a/src/masonite/utils/collections.py b/src/masonite/utils/collections.py new file mode 100644 index 000000000..441f552f8 --- /dev/null +++ b/src/masonite/utils/collections.py @@ -0,0 +1,545 @@ +import json +import random +import operator +from functools import reduce +from dotty_dict import Dotty + +from .structures import data_get + + +class Collection: + """Wraps various data types to make working with them easier.""" + + def __init__(self, items=None): + self._items = items or [] + self.__appends__ = [] + + def take(self, number: int): + """Takes a specific number of results from the items. + + Arguments: + number {integer} -- The number of results to take. + + Returns: + int + """ + if number < 0: + return self[number:] + + return self[:number] + + def first(self, callback=None): + """Takes the first result in the items. + + If a callback is given then the first result will be the result after the filter. + + Keyword Arguments: + callback {callable} -- Used to filter the results before returning the first item. (default: {None}) + + Returns: + mixed -- Returns whatever the first item is. + """ + filtered = self + if callback: + filtered = self.filter(callback) + response = None + if filtered: + response = filtered[0] + return response + + def last(self, callback=None): + """Takes the last result in the items. + + If a callback is given then the last result will be the result after the filter. + + Keyword Arguments: + callback {callable} -- Used to filter the results before returning the last item. (default: {None}) + + Returns: + mixed -- Returns whatever the last item is. + """ + filtered = self + if callback: + filtered = self.filter(callback) + return filtered[-1] + + def all(self): + """Returns all the items. + + Returns: + mixed -- Returns all items. + """ + return self._items + + def avg(self, key=None): + """Returns the average of the items. + + If a key is given it will return the average of all the values of the key. + + Keyword Arguments: + key {string} -- The key to use to find the average of all the values of that key. (default: {None}) + + Returns: + int -- Returns the average. + """ + result = 0 + items = self._get_value(key) or self._items + try: + result = sum(items) / len(items) + except TypeError: + pass + return result + + def max(self, key=None): + """Returns the average of the items. + + If a key is given it will return the average of all the values of the key. + + Keyword Arguments: + key {string} -- The key to use to find the average of all the values of that key. (default: {None}) + + Returns: + int -- Returns the average. + """ + result = 0 + items = self._get_value(key) or self._items + + try: + return max(items) + except (TypeError, ValueError): + pass + return result + + def chunk(self, size: int): + """Chunks the items. + + Keyword Arguments: + size {integer} -- The number of values in each chunk. + + Returns: + int -- Returns the average. + """ + items = [] + for i in range(0, self.count(), size): + items.append(self[i : i + size]) + return self.__class__(items) + + def collapse(self): + items = [] + for item in self: + items += self.__get_items(item) + return self.__class__(items) + + def contains(self, key, value=None): + if value: + return self.contains(lambda x: self._data_get(x, key) == value) + + if self._check_is_callable(key, raise_exception=False): + return self.first(key) is not None + + return key in self + + def count(self): + return len(self._items) + + def diff(self, items): + items = self.__get_items(items) + return self.__class__([x for x in self if x not in items]) + + def each(self, callback): + self._check_is_callable(callback) + + for k, v in enumerate(self): + result = callback(v) + if not result: + break + self[k] = result + + def every(self, callback): + self._check_is_callable(callback) + return all([callback(x) for x in self]) + + def filter(self, callback): + self._check_is_callable(callback) + return self.__class__(list(filter(callback, self))) + + def flatten(self): + def _flatten(items): + if isinstance(items, dict): + for v in items.values(): + for x in _flatten(v): + yield x + elif isinstance(items, list): + for i in items: + for j in _flatten(i): + yield j + else: + yield items + + return self.__class__(list(_flatten(self._items))) + + def forget(self, *keys): + keys = reversed(sorted(keys)) + + for key in keys: + del self[key] + + return self + + def for_page(self, page, number): + return self.__class__(self[page:number]) + + def get(self, key, default=None): + try: + return self[key] + except IndexError: + pass + + return self._value(default) + + def implode(self, glue=",", key=None): + first = self.first() + if not isinstance(first, str) and key: + return glue.join(self.pluck(key)) + return glue.join([str(x) for x in self]) + + def is_empty(self): + return not self + + def map(self, callback): + self._check_is_callable(callback) + items = [callback(x) for x in self] + return self.__class__(items) + + def map_into(self, cls, method=None, **kwargs): + results = [] + for item in self: + if method: + results.append(getattr(cls, method)(item, **kwargs)) + else: + results.append(cls(item)) + + return self.__class__(results) + + def merge(self, items): + if not isinstance(items, list): + raise ValueError("Unable to merge uncompatible types") + + items = self.__get_items(items) + + self._items += items + return self + + def pluck(self, value, key=None): + if key: + attributes = {} + else: + attributes = [] + + if isinstance(self._items, dict): + return Collection([self._items.get(value)]) + + for item in self: + if isinstance(item, dict): + iterable = item.items() + elif hasattr(item, "serialize"): + iterable = item.serialize().items() + else: + iterable = self.all().items() + + for k, v in iterable: + if k == value: + if key: + attributes[self._data_get(item, key)] = self._data_get( + item, value + ) + else: + attributes.append(v) + + return Collection(attributes) + + def pop(self): + last = self._items.pop() + return last + + def prepend(self, value): + self._items.insert(0, value) + return self + + def pull(self, key): + value = self.get(key) + self.forget(key) + return value + + def push(self, value): + self._items.append(value) + + def put(self, key, value): + self[key] = value + return self + + def random(self, count=None): + """Returns a random item of the collection.""" + collection_count = self.count() + if collection_count == 0: + return None + elif count and count > collection_count: + raise ValueError("count argument must be inferior to collection length.") + elif count: + self._items = random.sample(self._items, k=count) + return self + else: + return random.choice(self._items) + + def reduce(self, callback, initial=0): + return reduce(callback, self, initial) + + def reject(self, callback): + self._check_is_callable(callback) + + items = self._get_value(callback) or self._items + self._items = items + + def reverse(self): + self._items = self[::-1] + + def serialize(self): + def _serialize(item): + if self.__appends__: + item.set_appends(self.__appends__) + + if hasattr(item, "serialize"): + return item.serialize() + elif hasattr(item, "to_dict"): + return item.to_dict() + return item + + return list(map(_serialize, self)) + + def add_relation(self, result=None): + for model in self._items: + model.add_relations(result or {}) + + return self + + def shift(self): + return self.pull(0) + + def sort(self, key=None): + if key: + self._items.sort(key=lambda x: x[key], reverse=False) + return self + + self._items = sorted(self) + return self + + def sum(self, key=None): + result = 0 + items = self._get_value(key) or self._items + try: + result = sum(items) + except TypeError: + pass + return result + + def to_json(self, **kwargs): + return json.dumps(self.serialize(), **kwargs) + + def group_by(self, key): + + from itertools import groupby + + self.sort(key) + + new_dict = {} + + for k, v in groupby(self._items, key=lambda x: x[key]): + new_dict.update({k: list(v)}) + + return Collection(new_dict) + + def transform(self, callback): + self._check_is_callable(callback) + self._items = self._get_value(callback) + + def unique(self, key=None): + if not key: + items = list(set(self._items)) + return self.__class__(items) + + keys = set() + items = [] + if isinstance(self.all(), dict): + return self + + for item in self: + if isinstance(item, dict): + comparison = item.get(key) + elif isinstance(item, str): + comparison = item + else: + comparison = getattr(item, key) + if comparison not in keys: + items.append(item) + keys.add(comparison) + + return self.__class__(items) + + def where(self, key, *args): + op = "==" + value = args[0] + + if len(args) >= 2: + op = args[0] + value = args[1] + + attributes = [] + + for item in self._items: + if isinstance(item, dict): + comparison = item.get(key) + else: + comparison = getattr(item, key) + if self._make_comparison(comparison, value, op): + attributes.append(item) + + return self.__class__(attributes) + + def zip(self, items): + items = self.__get_items(items) + if not isinstance(items, list): + raise ValueError("The 'items' parameter must be a list or a Collection") + + _items = [] + for x, y in zip(self, items): + _items.append([x, y]) + return self.__class__(_items) + + def set_appends(self, appends): + """ + Set the attributes that should be appended to the Collection. + + :rtype: list + """ + self.__appends__ += appends + return self + + def _get_value(self, key): + if not key: + return None + + items = [] + for item in self: + if isinstance(key, str): + if hasattr(item, key) or (key in item): + items.append(getattr(item, key, item[key])) + elif callable(key): + result = key(item) + if result: + items.append(result) + return items + + def _data_get(self, item, key, default=None): + try: + if isinstance(item, (list, tuple)): + item = item[key] + elif isinstance(item, (dict, Dotty)): + item = data_get(item, key, default) + elif isinstance(item, object): + item = getattr(item, key) + except (IndexError, AttributeError, KeyError, TypeError): + return self._value(default) + + return item + + def _value(self, value): + if callable(value): + return value() + return value + + def _check_is_callable(self, callback, raise_exception=True): + if not callable(callback): + if not raise_exception: + return False + raise ValueError("The 'callback' should be a function") + return True + + def _make_comparison(self, a, b, op): + operators = { + "<": operator.lt, + "<=": operator.le, + "==": operator.eq, + "!=": operator.ne, + ">": operator.gt, + ">=": operator.ge, + } + return operators[op](a, b) + + def __iter__(self): + for item in self._items: + yield item + + def __eq__(self, other): + if isinstance(other, Collection): + return other == other.all() + return other == self._items + + def __getitem__(self, item): + if isinstance(item, slice): + return self.__class__(self._items[item]) + + return self._items[item] + + def __setitem__(self, key, value): + self._items[key] = value + + def __delitem__(self, key): + del self._items[key] + + def __ne__(self, other): + other = self.__get_items(other) + return other != self._items + + def __len__(self): + return len(self._items) + + def __le__(self, other): + other = self.__get_items(other) + return self._items <= other + + def __lt__(self, other): + other = self.__get_items(other) + return self._items < other + + def __ge__(self, other): + other = self.__get_items(other) + return self._items >= other + + def __gt__(self, other): + other = self.__get_items(other) + return self._items > other + + @classmethod + def __get_items(cls, items): + if isinstance(items, Collection): + items = items.all() + + return items + + +def collect(iterable): + """Transform an iterable into a collection.""" + return Collection(iterable) + + +def flatten(iterable): + """Flatten all sub-iterables of an iterable structure (recursively).""" + flat_list = [] + for item in iterable: + if isinstance(item, list): + for subitem in flatten(item): + flat_list.append(subitem) + else: + flat_list.append(item) + + return flat_list diff --git a/src/masonite/utils/console.py b/src/masonite/utils/console.py new file mode 100644 index 000000000..1bb23c017 --- /dev/null +++ b/src/masonite/utils/console.py @@ -0,0 +1,14 @@ +class HasColoredOutput: + """Add level-colored output print functions to a class.""" + + def success(self, message): + print("\033[92m {0} \033[0m".format(message)) + + def warning(self, message): + print("\033[93m {0} \033[0m".format(message)) + + def danger(self, message): + print("\033[91m {0} \033[0m".format(message)) + + def info(self, message): + return self.success(message) diff --git a/src/masonite/utils/filesystem.py b/src/masonite/utils/filesystem.py new file mode 100644 index 000000000..1b599249b --- /dev/null +++ b/src/masonite/utils/filesystem.py @@ -0,0 +1,72 @@ +import os +import platform + + +def make_directory(directory): + """Create a directory at the given path for a file if it does not exist""" + if not os.path.isfile(directory): + if not os.path.exists(os.path.dirname(directory)): + # Create the path to the model if it does not exist + os.makedirs(os.path.dirname(directory)) + + return True + + return False + + +def file_exists(directory): + """Create a directory at the given path for a file if it does not exist""" + return os.path.exists(os.path.dirname(directory)) + + +def make_full_directory(directory): + """Create all directories to the given path if they do not exist""" + if not os.path.isfile(directory): + if not os.path.exists(directory): + # Create the path to the model if it does not exist + os.makedirs(directory) + + return True + + return False + + +def creation_date(path_to_file): + """Try to get the date that a file was created, falling back to when it was + last modified if that isn't possible. + """ + if platform.system() == "Windows": + return os.path.getctime(path_to_file) + else: + stat = os.stat(path_to_file) + try: + return stat.st_birthtime + except AttributeError: + # We're probably on Linux. No easy way to get creation dates here, + # so we'll settle for when its content was last modified. + return stat.st_mtime + + +def modified_date(path_to_file): + if platform.system() == "Windows": + return os.path.getmtime(path_to_file) + else: + stat = os.stat(path_to_file) + try: + return stat.st_mtime + except AttributeError: + # We're probably on Linux. No easy way to get creation dates here, + # so we'll settle for when its content was last modified. + return 0 + + +def render_stub_file(stub_file, name): + """Read stub file, replace placeholders and return content.""" + with open(stub_file, "r") as f: + content = f.read() + content = content.replace("__class__", name) + return content + + +def get_module_dir(module_file): + return os.path.dirname(os.path.realpath(module_file)) diff --git a/src/masonite/helpers/status.py b/src/masonite/utils/http.py similarity index 58% rename from src/masonite/helpers/status.py rename to src/masonite/utils/http.py index fab711477..4a672a128 100644 --- a/src/masonite/helpers/status.py +++ b/src/masonite/utils/http.py @@ -1,6 +1,7 @@ -"""Helper Functions for working with Status Codes.""" +"""Helpers for working with HTTP.""" -statuses = { + +HTTP_STATUS_CODES = { 100: "100 Continue", 101: "101 Switching Protocol", 102: "102 Processing", @@ -65,5 +66,36 @@ } -def response_statuses(): - return statuses +def generate_wsgi(wsgi={}, path="/", query_string="application=Masonite", method="GET"): + """Generate the WSGI environment dictionary that we receive from a HTTP request.""" + import io + + data = { + "wsgi.version": (1, 0), + "wsgi.multithread": False, + "wsgi.multiprocess": True, + "wsgi.run_once": False, + "wsgi.input": io.BytesIO(), + "SERVER_SOFTWARE": "gunicorn/19.7.1", + "REQUEST_METHOD": method, + "QUERY_STRING": query_string, + "RAW_URI": path, + "SERVER_PROTOCOL": "HTTP/1.1", + "HTTP_HOST": "127.0.0.1:8000", + "HTTP_ACCEPT": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "HTTP_UPGRADE_INSECURE_REQUESTS": "1", + "HTTP_COOKIE": "setcookie=value", + "HTTP_USER_AGENT": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/604.4.7 (KHTML, like Gecko) Version/11.0.2 Safari/604.4.7", + "HTTP_ACCEPT_LANGUAGE": "en-us", + "HTTP_ACCEPT_ENCODING": "gzip, deflate", + "HTTP_CONNECTION": "keep-alive", + "wsgi.url_scheme": "http", + "REMOTE_ADDR": "127.0.0.1", + "REMOTE_PORT": "62241", + "SERVER_NAME": "127.0.0.1", + "SERVER_PORT": "8000", + "PATH_INFO": path, + "SCRIPT_NAME": "", + } + data.update(wsgi) + return data diff --git a/src/masonite/utils/location.py b/src/masonite/utils/location.py new file mode 100644 index 000000000..859d40c04 --- /dev/null +++ b/src/masonite/utils/location.py @@ -0,0 +1,74 @@ +"""Helpers to resolve absolute paths to the different app resources using a configured +location.""" +from os.path import join, abspath + +from .str import as_filepath + + +def _build_path(location_key, relative_path, absolute): + from wsgi import application + + relative_dir = join(as_filepath(application.make(location_key)), relative_path) + return abspath(relative_dir) if absolute else relative_dir + + +def base_path(relative_path=""): + """Build the absolute path to the project root directory or build the absolute path to a + given file relative to the project root directory.""" + return abspath(relative_path) + + +def views_path(relative_path="", absolute=True): + """Build the absolute path to the project views directory or build the absolute path to a given + file relative the project views directory. + + The relative path can be returned instead by setting absolute=False.""" + return _build_path("views.location", relative_path, absolute) + + +def controllers_path(relative_path="", absolute=True): + """Build the absolute path to the project controllers directory or build the absolute path to a given + file relative the project controllers directory. + + The relative path can be returned instead by setting absolute=False.""" + return _build_path("controllers.location", relative_path, absolute) + + +def config_path(relative_path="", absolute=True): + """Build the absolute path to the project configuration directory or build the absolute path to a given + file relative the project configuration directory. + + The relative path can be returned instead by setting absolute=False.""" + return _build_path("config.location", relative_path, absolute) + + +def migrations_path(relative_path="", absolute=True): + """Build the absolute path to the project migrations directory or build the absolute path to a given + file relative the project migrations directory. + + The relative path can be returned instead by setting absolute=False.""" + return _build_path("migrations.location", relative_path, absolute) + + +def seeds_path(relative_path="", absolute=True): + """Build the absolute path to the project seeds directory or build the absolute path to a given + file relative the project seeds directory. + + The relative path can be returned instead by setting absolute=False.""" + return _build_path("seeds.location", relative_path, absolute) + + +def jobs_path(relative_path="", absolute=True): + """Build the absolute path to the project jobs directory or build the absolute path to a given + file relative the project jobs directory. + + The relative path can be returned instead by setting absolute=False.""" + return _build_path("jobs.location", relative_path, absolute) + + +def resources_path(relative_path="", absolute=True): + """Build the absolute path to the project resources directory or build the absolute path to a given + file relative the project resources directory. + + The relative path can be returned instead by setting absolute=False.""" + return _build_path("resources.location", relative_path, absolute) diff --git a/src/masonite/utils/str.py b/src/masonite/utils/str.py new file mode 100644 index 000000000..2bb8d5bc6 --- /dev/null +++ b/src/masonite/utils/str.py @@ -0,0 +1,58 @@ +"""String generators and helpers""" +import random +import string + + +def random_string(length=4): + """Generate a random string based on the given length. + + Keyword Arguments: + length {int} -- The amount of the characters to generate (default: {4}) + + Returns: + string + """ + return "".join( + random.choice(string.ascii_uppercase + string.digits) for _ in range(length) + ) + + +def modularize(file_path, suffix=".py"): + """Transforms a file path to a dotted path. + + Keyword Arguments: + file_path {str} -- A file path such app/controllers + + Returns: + value {str} -- a dotted path such as app.controllers + """ + # if the file had the .py extension remove it as it's not needed for a module + return removesuffix(file_path.replace("/", "."), suffix) + + +def as_filepath(dotted_path): + """Inverse of modularize, transforms a dotted path to a file path (with /). + + Keyword Arguments: + dotted_path {str} -- A dotted path such app.controllers + + Returns: + value {str} -- a file path such as app/controllers + """ + return dotted_path.replace(".", "/") + + +def removeprefix(string, prefix): + """Implementation of str.removeprefix() function available for Python versions lower than 3.9.""" + if string.startswith(prefix): + return string[len(prefix) :] + else: + return string + + +def removesuffix(string, suffix): + """Implementation of str.removesuffix() function available for Python versions lower than 3.9.""" + if suffix and string.endswith(suffix): + return string[: -len(suffix)] + else: + return string diff --git a/src/masonite/utils/structures.py b/src/masonite/utils/structures.py new file mode 100644 index 000000000..6d42ec93a --- /dev/null +++ b/src/masonite/utils/structures.py @@ -0,0 +1,93 @@ +"""Helpers for multiple data structures""" +import importlib +from importlib.abc import Loader +from dotty_dict import dotty + +from ..exceptions.exceptions import LoaderNotFound + +from .str import modularize + + +def load(path, object_name=None, default=None, raise_exception=False): + """Load the given object from a Python module located at path and returns a default + value if not found. If no object name is provided, loads the module. + + Arguments: + path {str} -- A file path or a dotted path of a Python module + object {str} -- The object name to load in this module (None) + default {str} -- The default value to return if object not found in module (None) + Returns: + {object} -- The value (or default) read in the module or the module if no object name + """ + # modularize path if needed + module_path = modularize(path) + # module = pydoc.locate(dotted_path) + try: + module = importlib.import_module(module_path) + except ModuleNotFoundError: + if raise_exception: + raise LoaderNotFound( + f"{module_path} not found or error when importing this module." + ) + return None + + if object_name is None: + return module + else: + try: + return getattr(module, object_name) + except KeyError: + if raise_exception: + raise LoaderNotFound(f"{object_name} not found in {module_path}") + else: + return default + + +def data(dictionary={}): + """Transform the given dictionary to be read/written with dot notation. + + Arguments: + dictionary {dict} -- a dictionary structure + + Returns: + {dict} -- A dot dictionary + """ + return dotty(dictionary) + + +def data_get(dictionary, key, default=None): + """Read dictionary value from key using nested notation. + + Arguments: + dictionary {dict} -- a dictionary structure + key {str} -- the dotted (or not) key to look for + default {object} -- the default value to return if the key does not exist (None) + + Returns: + value {object} + """ + # dotty dict uses : instead of * for wildcards + dotty_key = key.replace("*", ":") + return data(dictionary).get(dotty_key, default) + + +def data_set(dictionary, key, value, overwrite=True): + """Set dictionary value at key using nested notation. Values are overriden by default but + this behaviour can be changed by passing overwrite=False. + The dictionary is edited in place but is also returned. + + Arguments: + dictionary {dict} -- a dictionary structure + key {str} -- the dotted (or not) key to set + value {object} -- the value to set at key + overwrite {bool} -- override the value if key exists in dictionary (True) + + Returns: + dictionary {dict} -- the edited dictionary + """ + if "*" in key: + raise ValueError("You cannot set values with wildcards *") + if not overwrite and data_get(dictionary, key): + return + data(dictionary)[key] = value + return dictionary diff --git a/src/masonite/utils/time.py b/src/masonite/utils/time.py new file mode 100644 index 000000000..ecbf7cc31 --- /dev/null +++ b/src/masonite/utils/time.py @@ -0,0 +1,58 @@ +"""Time related helpers""" +import pendulum + + +def cookie_expire_time(str_time): + """Take a string like 1 month or 5 minutes and returns a datetime formatted with cookie format. + + Arguments: + str_time {string} -- Could be values like 1 second or 3 minutes + + Returns: + str -- Cookie expiration time (Thu, 21 Oct 2021 07:28:00) + """ + instance = parse_human_time(str_time) + return instance.format("ddd, DD MMM YYYY HH:mm:ss") + + +def parse_human_time(str_time): + """Take a string like 1 month or 5 minutes and returns a pendulum instance. + + Arguments: + str_time {string} -- Could be values like 1 second or 3 minutes + + Returns: + pendulum -- Returns Pendulum instance + """ + if str_time == "now": + return pendulum.now("GMT") + + if str_time != "expired": + number = int(str_time.split(" ")[0]) + length = str_time.split(" ")[1] + + if length in ("second", "seconds"): + return pendulum.now("GMT").add(seconds=number) + elif length in ("minute", "minutes"): + return pendulum.now("GMT").add(minutes=number) + elif length in ("hour", "hours"): + return pendulum.now("GMT").add(hours=number) + elif length in ("day", "days"): + return pendulum.now("GMT").add(days=number) + elif length in ("week", "weeks"): + return pendulum.now("GMT").add(weeks=number) + elif length in ("month", "months"): + return pendulum.now("GMT").add(months=number) + elif length in ("year", "years"): + return pendulum.now("GMT").add(years=number) + + return None + else: + return pendulum.now("GMT").subtract(years=20) + + +def migration_timestamp(): + """Return current time formatted for creating migration filenames. + Example: 2021_01_09_043202 + """ + return pendulum.now().format("YYYY_MM_DD_HHmmss") diff --git a/src/masonite/validation/MessageBag.py b/src/masonite/validation/MessageBag.py new file mode 100644 index 000000000..2df8ed1e0 --- /dev/null +++ b/src/masonite/validation/MessageBag.py @@ -0,0 +1,120 @@ +"""The Message Bag Module""" + +import json + + +class MessageBag: + def __init__(self, items={}): + self.items = items + + def add(self, error, message): + """Adds an error and message to the message bag + + Arguments: + error {string} -- The error to add + message {string} -- The message to add + """ + + if error in self.items: + self.items[error].append(message) + else: + self.items.update({error: [message]}) + + def all(self): + """Get all errors and messages""" + return self.items + + def any(self): + """If the messagebag has any errors""" + return len(self.items) > 0 + + def has(self, key): + """If the messagebag has any errors""" + return key in self.all() + + def empty(self): + """If the messagebag has any errors""" + return not self.any() + + def first(self, key): + """Gets the first error and message""" + return self.get(key)[0] + + def count(self): + """Gets the amount of errors""" + return len(self.items) + + def json(self): + """Gets the amount of errors""" + return json.dumps(self.items) + + def amount(self, key): + """Gets the amount of messages + + Arguments: + key {string} -- the error to get the amount of. + + Returns: + int -- Returns the amount of messages + """ + return len(self.items[key]) + + def get(self, key): + """Gets all the messages for a specific error. + + Arguments: + key {string} -- the error to get the messages for + + Returns: + list -- list of errors + """ + return self.items[key] + + def errors(self): + """Gets a list of errors""" + return list(self.items.keys()) + + def messages(self): + """Gets a list of all the messages""" + messages = [] + for error, message in self.items.items(): + messages += message + + return messages + + def reset(self): + """Gets a list of all the messages""" + self.items = {} + + def merge(self, dictionary): + """Merge a dictionary into the message bag. + + Arguments: + dictionary {dict} -- dictionary of errors and messages. + + Returns: + dictionary -- Returns a dictionary of the new errors. + """ + self.items.update(dictionary) + return self.any() + + def new(self, dictionary): + return self.__class__(dictionary) + + def __len__(self): + return len(self.items) + + def __str__(self): + return json.dumps(self.items) + + def get_response(self): + return json.dumps(self.items) + + @staticmethod + def view_helper(errors={}): + if errors: + return MessageBag(errors) + + from wsgi import application + + return MessageBag(application.make("request").session.get("errors") or {}) diff --git a/src/masonite/validation/RuleEnclosure.py b/src/masonite/validation/RuleEnclosure.py new file mode 100644 index 000000000..db381b4b3 --- /dev/null +++ b/src/masonite/validation/RuleEnclosure.py @@ -0,0 +1,2 @@ +class RuleEnclosure: + pass diff --git a/src/masonite/validation/Validator.py b/src/masonite/validation/Validator.py new file mode 100644 index 000000000..2a19cd674 --- /dev/null +++ b/src/masonite/validation/Validator.py @@ -0,0 +1,1308 @@ +from .RuleEnclosure import RuleEnclosure +from .MessageBag import MessageBag +from ..utils.structures import data_get +import inspect +import re +import os +import mimetypes + + +class BaseValidation: + def __init__(self, validations, messages={}, raises={}): + self.errors = {} + self.messages = messages + if isinstance(validations, str): + self.validations = [validations] + else: + self.validations = validations + self.negated = False + self.raises = raises + + def passes(self, attribute, key, dictionary): + return True + + def error(self, key, message): + if key in self.messages: + if key in self.errors: + self.errors[key].append(self.messages[key]) + return + self.errors.update({key: [self.messages[key]]}) + return + + if not isinstance(message, list): + self.errors.update({key: [message]}) + else: + self.errors.update({key: message}) + + def find(self, key, dictionary, default=False): + return data_get(dictionary, key, default) + + def message(self, key): + return "" + + def negate(self): + self.negated = True + return self + + def raise_exception(self, key): + if self.raises is not True and key in self.raises: + error = self.raises.get(key) + raise error(self.errors[next(iter(self.errors))][0]) + + raise ValueError(self.errors[next(iter(self.errors))][0]) + + def handle(self, dictionary): + boolean = True + + for key in self.validations: + if self.negated: + + if self.passes(self.find(key, dictionary), key, dictionary): + boolean = False + if hasattr(self, "negated_message"): + self.error(key, self.negated_message(key)) + else: + self.error(key, self.message(key)) + + continue + attribute = self.find(key, dictionary) + if not self.passes(attribute, key, dictionary): + boolean = False + self.error(key, self.message(key)) + + if self.errors and self.raises: + return self.raise_exception(key) + + return boolean + + def reset(self): + self.errors = {} + + +class required(BaseValidation): + def passes(self, attribute, key, dictionary): + """The passing criteria for this rule. + + The key must exist in the dictionary and return a True boolean value. + The key can use * notation. + + Arguments: + attribute {mixed} -- The value found within the dictionary + key {string} -- The key in the dictionary being searched for. + dictionary {dict} -- The dictionary being searched + + Returns: + bool + """ + return self.find(key, dictionary) and attribute + + def message(self, key): + """A message to show when this rule fails + + Arguments: + key {string} -- The key used to search the dictionary + + Returns: + string + """ + return "The {} field is required.".format(key) + + def negated_message(self, key): + """A message to show when this rule is negated using a negation rule like 'isnt()' + + For example if you have a message that says 'this is required' you may have a negated statement + that says 'this is not required'. + + Arguments: + key {string} -- The key used to search the dictionary + + Returns: + string + """ + return "The {} field is not required.".format(key) + + +class timezone(BaseValidation): + def passes(self, attribute, key, dictionary): + import pytz + + return attribute in pytz.all_timezones + + def message(self, attribute): + return "The {} must be a valid timezone.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a valid timezone.".format(attribute) + + +class one_of(BaseValidation): + def passes(self, attribute, key, dictionary): + for validation in self.validations: + if validation in dictionary: + return True + + return False + + def message(self, attribute): + if len(self.validations) > 2: + text = ", ".join(self.validations) + else: + text = " or ".join(self.validations) + + return "The {} is required.".format(text) + + def negated_message(self, attribute): + if len(self.validations) > 2: + text = ", ".join(self.validations) + else: + text = " or ".join(self.validations) + + return "The {} is not required.".format(text) + + +class accepted(BaseValidation): + def passes(self, attribute, key, dictionary): + return ( + attribute is True + or attribute == "on" + or attribute == "yes" + or attribute == "1" + or attribute == 1 + ) + + def message(self, attribute): + return "The {} must be accepted.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be accepted.".format(attribute) + + +class ip(BaseValidation): + def passes(self, attribute, key, dictionary): + import socket + + try: + socket.inet_aton(attribute) + return True + except socket.error: + return False + + def message(self, attribute): + return "The {} must be a valid ipv4 address.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a valid ipv4 address.".format(attribute) + + +class date(BaseValidation): + def passes(self, attribute, key, dictionary): + import pendulum + + try: + date = pendulum.parse(attribute) + return date + except pendulum.parsing.exceptions.ParserError: + return False + + def message(self, attribute): + return "The {} must be a valid date.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a valid date.".format(attribute) + + +class before_today(BaseValidation): + def __init__(self, validations, tz="UTC", messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.tz = tz + + def passes(self, attribute, key, dictionary): + import pendulum + + try: + return pendulum.parse(attribute, tz=self.tz) <= pendulum.yesterday() + except pendulum.parsing.exceptions.ParserError: + return False + + def message(self, attribute): + return "The {} must be a date before today.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a date before today.".format(attribute) + + +class after_today(BaseValidation): + def __init__(self, validations, tz="Universal", messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.tz = tz + + def passes(self, attribute, key, dictionary): + import pendulum + + try: + return pendulum.parse(attribute, tz=self.tz) >= pendulum.yesterday() + except pendulum.parsing.exceptions.ParserError: + return False + + def message(self, attribute): + return "The {} must be a date after today.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a date after today.".format(attribute) + + +class is_past(BaseValidation): + def __init__(self, validations, tz="Universal", messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.tz = tz + + def passes(self, attribute, key, dictionary): + import pendulum + + try: + return pendulum.parse(attribute, tz=self.tz).is_past() + except pendulum.parsing.exceptions.ParserError: + return False + + def message(self, attribute): + return "The {} must be a time in the past.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a time in the past.".format(attribute) + + +class is_future(BaseValidation): + def __init__(self, validations, tz="Universal", messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.tz = tz + + def passes(self, attribute, key, dictionary): + import pendulum + + try: + return pendulum.parse(attribute, tz=self.tz).is_future() + except pendulum.parsing.exceptions.ParserError: + return False + + def message(self, attribute): + return "The {} must be a time in the past.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a time in the past.".format(attribute) + + +class email(BaseValidation): + def passes(self, attribute, key, dictionary): + return re.compile( + r"^[^.][^@]*@([?)[a-zA-Z0-9-.])+.([a-zA-Z]{2,3}|[0-9]{1,3})(]?)$" + ).match(attribute) + + def message(self, attribute): + return "The {} must be a valid email address.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a valid email address.".format(attribute) + + +class matches(BaseValidation): + def __init__(self, validations, match, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.match = match + + def passes(self, attribute, key, dictionary): + return attribute == dictionary[self.match] + + def message(self, attribute): + return "The {} must match {}.".format(attribute, self.match) + + def negated_message(self, attribute): + return "The {} must not match {}.".format(attribute, self.match) + + +class exists(BaseValidation): + def passes(self, attribute, key, dictionary): + return key in dictionary + + def message(self, attribute): + return "The {} must exist.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not exist.".format(attribute) + + +class active_domain(BaseValidation): + def passes(self, attribute, key, dictionary): + import socket + + try: + if "@" in attribute: + # validation is for an email address + return socket.gethostbyname(attribute.split("@")[1]) + + return socket.gethostbyname( + attribute.replace("https://", "") + .replace("http://", "") + .replace("www.", "") + ) + except socket.gaierror: + return False + + def message(self, attribute): + return "The {} must be an active domain name.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be an active domain name.".format(attribute) + + +class numeric(BaseValidation): + def passes(self, attribute, key, dictionary): + if isinstance(attribute, list): + for value in attribute: + if not str(value).isdigit(): + return False + else: + return str(attribute).isdigit() + + return True + + def message(self, attribute): + return "The {} must be a numeric.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a numeric.".format(attribute) + + +class is_list(BaseValidation): + def passes(self, attribute, key, dictionary): + return isinstance(attribute, list) + + def message(self, attribute): + return "The {} must be a list.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a list.".format(attribute) + + +class string(BaseValidation): + def passes(self, attribute, key, dictionary): + if isinstance(attribute, list): + for attr in attribute: + if not isinstance(attr, str): + return False + + return True + + return isinstance(attribute, str) + + def message(self, attribute): + return "The {} must be a string.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a string.".format(attribute) + + +class none(BaseValidation): + def passes(self, attribute, key, dictionary): + return attribute is None + + def message(self, attribute): + return "The {} must be None.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be None.".format(attribute) + + +class length(BaseValidation): + def __init__(self, validations, min=0, max=False, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + if isinstance(min, str) and ".." in min: + self.min = int(min.split("..")[0]) + self.max = int(min.split("..")[1]) + else: + self.min = min + self.max = max + + def passes(self, attribute, key, dictionary): + if not hasattr(attribute, "__len__"): + attribute = str(attribute) + if self.max: + return len(attribute) >= self.min and len(attribute) <= self.max + else: + return len(attribute) >= self.min + + def message(self, attribute): + if self.min and not self.max: + return "The {} must be at least {} characters.".format(attribute, self.min) + else: + return "The {} length must be between {} and {}.".format( + attribute, self.min, self.max + ) + + def negated_message(self, attribute): + if self.min and not self.max: + return "The {} must be {} characters maximum.".format(attribute, self.max) + else: + return "The {} length must not be between {} and {}.".format( + attribute, self.min, self.max + ) + + +class in_range(BaseValidation): + def __init__(self, validations, min=1, max=255, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.min = min + self.max = max + + def passes(self, attribute, key, dictionary): + + attribute = str(attribute) + + if attribute.isalpha(): + return False + + if "." in attribute: + try: + attribute = float(attribute) + except Exception: + pass + + elif attribute.isdigit(): + attribute = int(attribute) + + return attribute >= self.min and attribute <= self.max + + def message(self, attribute): + return "The {} must be between {} and {}.".format(attribute, self.min, self.max) + + def negated_message(self, attribute): + return "The {} must not be between {} and {}.".format( + attribute, self.min, self.max + ) + + +class equals(BaseValidation): + def __init__(self, validations, value="", messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.value = value + + def passes(self, attribute, key, dictionary): + return attribute == self.value + + def message(self, attribute): + return "The {} must be equal to {}.".format(attribute, self.value) + + def negated_message(self, attribute): + return "The {} must not be equal to {}.".format(attribute, self.value) + + +class contains(BaseValidation): + def __init__(self, validations, value="", messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.value = value + + def passes(self, attribute, key, dictionary): + return self.value in attribute + + def message(self, attribute): + return "The {} must contain {}.".format(attribute, self.value) + + def negated_message(self, attribute): + return "The {} must not contain {}.".format(attribute, self.value) + + +class is_in(BaseValidation): + def __init__(self, validations, value="", messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.value = value + + def passes(self, attribute, key, dictionary): + return attribute in self.value + + def message(self, attribute): + return "The {} must contain an element in {}.".format(attribute, self.value) + + def negated_message(self, attribute): + return "The {} must not contain an element in {}.".format(attribute, self.value) + + +class greater_than(BaseValidation): + def __init__(self, validations, value="", messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.value = value + + def passes(self, attribute, key, dictionary): + return attribute > self.value + + def message(self, attribute): + return "The {} must be greater than {}.".format(attribute, self.value) + + def negated_message(self, attribute): + return "The {} must be greater than {}.".format(attribute, self.value) + + +class less_than(BaseValidation): + def __init__(self, validations, value="", messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.value = value + + def passes(self, attribute, key, dictionary): + return attribute < self.value + + def message(self, attribute): + return "The {} must be less than {}.".format(attribute, self.value) + + def negated_message(self, attribute): + return "The {} must not be less than {}.".format(attribute, self.value) + + +class strong(BaseValidation): + def __init__( + self, + validations, + length=8, + uppercase=2, + numbers=2, + special=2, + breach=False, + messages={}, + raises={}, + ): + super().__init__(validations, messages=messages, raises=raises) + self.length = length + self.uppercase = uppercase + self.numbers = numbers + self.special = special + self.breach = breach + self.length_check = True + self.uppercase_check = True + self.numbers_check = True + self.special_check = True + self.breach_check = True + + def passes(self, attribute, key, dictionary): + all_clear = True + + if len(attribute) < self.length: + all_clear = False + self.length_check = False + + if self.uppercase != 0: + uppercase = 0 + for letter in attribute: + if letter.isupper(): + uppercase += 1 + + if uppercase < self.uppercase: + self.uppercase_check = False + all_clear = False + + if self.numbers != 0: + numbers = 0 + for letter in attribute: + if letter.isdigit(): + numbers += 1 + + if numbers < self.numbers: + self.numbers_check = False + all_clear = False + + if self.breach: + try: + from pwnedapi import Password + except ImportError: + raise ImportError( + "Checking for breaches requires the 'pwnedapi' library. Please install it with 'pip install pwnedapi'" + ) + + password = Password(attribute) + if password.is_pwned(): + self.breach_check = False + all_clear = False + + if self.special != 0: + if len(re.findall("[^A-Za-z0-9]", attribute)) < self.special: + self.special_check = False + all_clear = False + + return all_clear + + def message(self, attribute): + message = [] + if not self.length_check: + message.append( + "The {} field must be {} characters in length".format( + attribute, self.length + ) + ) + + if not self.uppercase_check: + message.append( + "The {} field must have {} uppercase letters".format( + attribute, self.uppercase + ) + ) + + if not self.special_check: + message.append( + "The {} field must have {} special characters".format( + attribute, self.special + ) + ) + + if not self.numbers_check: + message.append( + "The {} field must have {} numbers".format(attribute, self.numbers) + ) + + if not self.breach_check: + message.append( + "The {} field has been breached in the past. Try another {}".format( + attribute, attribute + ) + ) + + return message + + def negated_message(self, attribute): + return "The {} must not be less than {}.".format(attribute, self.value) + + +class isnt(BaseValidation): + def __init__(self, *rules, messages={}, raises={}): + super().__init__(rules) + + def handle(self, dictionary): + for rule in self.validations: + rule.negate().handle(dictionary) + self.errors.update(rule.errors) + + +class does_not(BaseValidation): + def __init__(self, *rules, messages={}, raises={}): + super().__init__(rules) + self.should_run_then = True + + def handle(self, dictionary): + self.dictionary = dictionary + errors = False + for rule in self.validations: + if rule.handle(dictionary): + errors = True + + if not errors: + for rule in self.then_rules: + if not rule.handle(dictionary): + self.errors.update(rule.errors) + + def then(self, *rules): + self.then_rules = rules + return self + + +class when(BaseValidation): + def __init__(self, *rules, messages={}, raises={}): + super().__init__(rules) + self.should_run_then = True + + def handle(self, dictionary): + self.dictionary = dictionary + errors = False + for rule in self.validations: + if rule.handle(dictionary): + errors = True + + if errors: + for rule in self.then_rules: + if not rule.handle(dictionary): + self.errors.update(rule.errors) + + def then(self, *rules): + self.then_rules = rules + return self + + +class truthy(BaseValidation): + def passes(self, attribute, key, dictionary): + return attribute + + def message(self, attribute): + return "The {} must be a truthy value.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a truthy value.".format(attribute) + + +class json(BaseValidation): + def passes(self, attribute, key, dictionary): + import json as json_module + + try: + return json_module.loads(str(attribute)) + except (TypeError, json_module.decoder.JSONDecodeError): + return False + + def message(self, attribute): + return "The {} must be a valid JSON.".format(attribute) + + def negated_message(self, attribute): + return "The {} must not be a valid JSON.".format(attribute) + + +class phone(BaseValidation): + def __init__(self, *rules, pattern="123-456-7890", messages={}, raises={}): + super().__init__(rules, messages={}, raises={}) + # 123-456-7890 + # (123)456-7890 + self.pattern = pattern + + def passes(self, attribute, key, dictionary): + if self.pattern == "(123)456-7890": + return re.compile(r"^\(\w{3}\)\w{3}\-\w{4}$").match(attribute) + elif self.pattern == "123-456-7890": + return re.compile(r"^\w{3}\-\w{3}\-\w{4}$").match(attribute) + + def message(self, attribute): + if self.pattern == "(123)456-7890": + return "The {} must be in the format (XXX)XXX-XXXX.".format(attribute) + elif self.pattern == "123-456-7890": + return "The {} must be in the format XXX-XXX-XXXX.".format(attribute) + + def negated_message(self, attribute): + if self.pattern == "(123)456-7890": + return "The {} must not be in the format (XXX)XXX-XXXX.".format(attribute) + elif self.pattern == "123-456-7890": + return "The {} must not be in the format XXX-XXX-XXXX.".format(attribute) + + +class confirmed(BaseValidation): + def passes(self, attribute, key, dictionary): + if key in dictionary and key + "_confirmation" in dictionary: + return dictionary[key] == dictionary["{}".format(key + "_confirmation")] + return False + + def message(self, attribute): + return "The {} confirmation does not match.".format(attribute) + + def negated_message(self, attribute): + return "The {} confirmation matches.".format(attribute) + + +class regex(BaseValidation): + def __init__(self, validations, pattern, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.pattern = pattern + + def passes(self, attribute, key, dictionary): + return re.compile(r"{}".format(self.pattern)).match(attribute) + + def message(self, attribute): + return "The {} does not match pattern {} .".format(attribute, self.pattern) + + def negated_message(self, attribute): + return "The {} matches pattern {} .".format(attribute, self.pattern) + + +def parse_size(size): + """Parse humanized size into bytes""" + from hfilesize import FileSize + + return FileSize(size, case_sensitive=False) + + +class BaseFileValidation(BaseValidation): + def __init__(self, validations, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.file_check = True + self.size_check = True + self.mimes_check = True + self.all_clear = True + + def passes(self, attribute, key, dictionary): + if not os.path.isfile(attribute): + self.file_check = False + return False + if self.size: + file_size = os.path.getsize(attribute) + if file_size > self.size: + self.size_check = False + self.all_clear = False + if self.allowed_extensions: + mimetype, encoding = mimetypes.guess_type(attribute) + if mimetype not in self.allowed_mimetypes: + self.mimes_check = False + self.all_clear = False + return self.all_clear + + +class file(BaseFileValidation): + def __init__(self, validations, size=False, mimes=False, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.size = parse_size(size) + + # parse allowed extensions to a list of mime types + self.allowed_extensions = mimes + if mimes: + self.allowed_mimetypes = list( + map(lambda mt: mimetypes.types_map.get("." + mt, None), mimes) + ) + + def message(self, attribute): + messages = [] + if not self.file_check: + messages.append("The {} is not a valid file.".format(attribute)) + + if not self.size_check: + from hfilesize import FileSize + + messages.append( + "The {} file size exceeds {:.02fH}.".format( + attribute, FileSize(self.size) + ) + ) + if not self.mimes_check: + messages.append( + "The {} mime type is not valid. Allowed formats are {}.".format( + attribute, ",".join(self.allowed_extensions) + ) + ) + + return messages + + def negated_message(self, attribute): + messages = [] + if self.file_check: + messages.append("The {} is a valid file.".format(attribute)) + if self.size_check: + from hfilesize import FileSize + + messages.append( + "The {} file size is less or equal than {:.02fH}.".format( + attribute, FileSize(self.size) + ) + ) + if self.mimes_check: + messages.append( + "The {} mime type is in {}.".format( + attribute, ",".join(self.allowed_extensions) + ) + ) + return messages + + +class image(BaseFileValidation): + def __init__(self, validations, size=False, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.size = parse_size(size) + image_mimetypes = { + ext: mimetype + for ext, mimetype in mimetypes.types_map.items() + if mimetype.startswith("image") + } + self.allowed_extensions = list(image_mimetypes.keys()) + self.allowed_mimetypes = list(image_mimetypes.values()) + + def message(self, attribute): + messages = [] + if not self.file_check: + messages.append("The {} is not a valid file.".format(attribute)) + + if not self.size_check: + from hfilesize import FileSize + + messages.append( + "The {} file size exceeds {:.02fH}.".format( + attribute, FileSize(self.size) + ) + ) + + if not self.mimes_check: + messages.append( + "The {} file is not a valid image. Allowed formats are {}.".format( + attribute, ",".join(self.allowed_extensions) + ) + ) + + return messages + + def negated_message(self, attribute): + messages = [] + if self.file_check: + messages.append("The {} is a valid file.".format(attribute)) + if self.size_check: + from hfilesize import FileSize + + messages.append( + "The {} file size is less or equal than {:.02fH}.".format( + attribute, FileSize(self.size) + ) + ) + + if self.mimes_check: + messages.append("The {} file is a valid image.".format(attribute)) + + return messages + + +class video(BaseFileValidation): + def __init__(self, validations, size=False, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.size = parse_size(size) + + video_mimetypes = { + ext: mimetype + for ext, mimetype in mimetypes.types_map.items() + if mimetype.startswith("video") + } + + self.allowed_extensions = list(video_mimetypes.keys()) + self.allowed_mimetypes = list(video_mimetypes.values()) + + def message(self, attribute): + messages = [] + if not self.file_check: + messages.append("The {} is not a valid file.".format(attribute)) + + if not self.size_check: + from hfilesize import FileSize + + messages.append( + "The {} file size exceeds {:.02fH}.".format( + attribute, FileSize(self.size) + ) + ) + + if not self.mimes_check: + messages.append( + "The {} file is not a valid video. Allowed formats are {}.".format( + attribute, ",".join(self.allowed_extensions) + ) + ) + + return messages + + def negated_message(self, attribute): + messages = [] + if self.file_check: + messages.append("The {} is a valid file.".format(attribute)) + + if self.size_check: + from hfilesize import FileSize + + messages.append( + "The {} file size is less or equal than {:.02fH}.".format( + attribute, FileSize(self.size) + ) + ) + + if self.mimes_check: + messages.append("The {} file is a valid video.".format(attribute)) + + return messages + + +class postal_code(BaseValidation): + def __init__(self, validations, locale, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + from .resources.postal_codes import PATTERNS + + self.locales = [] + self.patterns = [] + self.patterns_example = [] + self.locales = locale.split(",") + + for locale in self.locales: + pattern_dict = PATTERNS.get(locale, None) + if pattern_dict is None or pattern_dict["pattern"] is None: + raise NotImplementedError( + "Unsupported country code {}. Check that it is a ISO 3166-1 country code or open a PR to require support of this country code.".format( + locale + ) + ) + else: + self.patterns.append(pattern_dict["pattern"]) + self.patterns_example.append(pattern_dict["example"]) + + def passes(self, attribute, key, dictionary): + for pattern in self.patterns: + # check that at least one pattern match attribute + if re.compile(r"{}".format(pattern)).match(attribute): + return True + return False + + def message(self, attribute): + return "The {} is not a valid {} postal code. Valid {} {}.".format( + attribute, + ",".join(self.locales), + "examples are" if len(self.locales) > 1 else "example is", + ",".join(self.patterns_example), + ) + + def negated_message(self, attribute): + return "The {} is a valid {} postal code.".format(attribute, self.locale) + + +class different(BaseValidation): + """The field under validation must be different than an other given field.""" + + def __init__(self, validations, other_field, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.other_field = other_field + + def passes(self, attribute, key, dictionary): + other_value = dictionary.get(self.other_field, None) + return attribute != other_value + + def message(self, attribute): + return "The {} value must be different than {} value.".format( + attribute, self.other_field + ) + + def negated_message(self, attribute): + return "The {} value be the same as {} value.".format( + attribute, self.other_field + ) + + +class uuid(BaseValidation): + """The field under validation must be a valid UUID. The UUID version standard + can be precised (1,3,4,5).""" + + def __init__(self, validations, version=4, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.version = version + self.uuid_type = "UUID" + if version: + self.uuid_type = "UUID {0}".format(self.version) + + def passes(self, attribute, key, dictionary): + from uuid import UUID + + try: + uuid_value = UUID(str(attribute)) + return uuid_value.version == int(self.version) + except ValueError: + return False + + def message(self, attribute): + return "The {} value must be a valid {}.".format(attribute, self.uuid_type) + + def negated_message(self, attribute): + return "The {} value must not be a valid {}.".format(attribute, self.uuid_type) + + +class required_if(BaseValidation): + """The field under validation must be present and not empty only + if an other field has a given value.""" + + def __init__(self, validations, other_field, value, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + self.other_field = other_field + self.value = value + + def passes(self, attribute, key, dictionary): + if dictionary.get(self.other_field, None) == self.value: + return required.passes(self, attribute, key, dictionary) + + return True + + def message(self, attribute): + return "The {} is required because {}={}.".format( + attribute, self.other_field, self.value + ) + + def negated_message(self, attribute): + return "The {} is not required because {}={} or {} is not present.".format( + attribute, self.other_field, self.value, self.other_field + ) + + +class required_with(BaseValidation): + """The field under validation must be present and not empty only + if any of the other specified fields are present.""" + + def __init__(self, validations, other_fields, messages={}, raises={}): + super().__init__(validations, messages=messages, raises=raises) + if not isinstance(other_fields, list): + if "," in other_fields: + self.other_fields = other_fields.split(",") + else: + self.other_fields = [other_fields] + else: + self.other_fields = other_fields + + def passes(self, attribute, key, dictionary): + for field in self.other_fields: + if field in dictionary: + return required.passes(self, attribute, key, dictionary) + else: + return True + + def message(self, attribute): + fields = ",".join(self.other_fields) + return "The {} is required because {} is present.".format( + attribute, + "one in {}".format(fields) + if len(self.other_fields) > 1 + else self.other_fields[0], + ) + + def negated_message(self, attribute): + return "The {} is not required because {} {} is not present.".format( + attribute, + "none of" if len(self.other_fields) > 1 else "", + ",".join(self.other_fields), + ) + + +class distinct(BaseValidation): + """When working with list, the field under validation must not have any + duplicate values.""" + + def passes(self, attribute, key, dictionary): + # check if list contains duplicates + return len(set(attribute)) == len(attribute) + + def message(self, attribute): + return "The {} field has duplicate values.".format(attribute) + + def negated_message(self, attribute): + return "The {} field has only different values.".format(attribute) + + +class Validator: + def __init__(self): + pass + + def validate(self, dictionary, *rules): + rule_errors = {} + try: + for rule in rules: + if isinstance(rule, str): + rule = self.parse_string(rule) + # continue + elif isinstance(rule, dict): + rule = self.parse_dict(rule, dictionary, rule_errors) + continue + + elif inspect.isclass(rule) and isinstance(rule(), RuleEnclosure): + rule_errors.update(self.run_enclosure(rule(), dictionary)) + continue + + rule.handle(dictionary) + for error, message in rule.errors.items(): + if error not in rule_errors: + rule_errors.update({error: message}) + else: + messages = rule_errors[error] + messages += message + rule_errors.update({error: messages}) + rule.reset() + return MessageBag(rule_errors) + + except Exception as e: + e.errors = rule_errors + raise e + + return MessageBag(rule_errors) + + def parse_string(self, rule): + rule, parameters = rule.split(":")[0], rule.split(":")[1].split(",") + return ValidationFactory().registry[rule](parameters) + + def parse_dict(self, rule, dictionary, rule_errors): + for value, rules in rule.items(): + for rule in rules.split("|"): + rule, args = rule.split(":")[0], rule.split(":")[1:] + rule = ValidationFactory().registry[rule](value, *args) + + rule.handle(dictionary) + for error, message in rule.errors.items(): + if error not in rule_errors: + rule_errors.update({error: message}) + else: + messages = rule_errors[error] + messages += message + rule_errors.update({error: messages}) + + def run_enclosure(self, enclosure, dictionary): + rule_errors = {} + for rule in enclosure.rules(): + rule.handle(dictionary) + for error, message in rule.errors.items(): + if error not in rule_errors: + rule_errors.update({error: message}) + else: + messages = rule_errors[error] + messages += message + rule_errors.update({error: messages}) + rule.reset() + return rule_errors + + def extend(self, key, obj=None): + if isinstance(key, dict): + self.__dict__.update(key) + return self + + self.__dict__.update({key: obj}) + return self + + def register(self, *cls): + for obj in cls: + self.__dict__.update({obj.__name__: obj}) + ValidationFactory().register(obj) + + +class ValidationFactory: + + registry = {} + + def __init__(self): + self.register( + accepted, + active_domain, + after_today, + before_today, + confirmed, + contains, + date, + does_not, + different, + distinct, + equals, + email, + exists, + file, + greater_than, + image, + in_range, + is_future, + is_in, + isnt, + is_list, + is_past, + ip, + json, + length, + less_than, + matches, + none, + numeric, + one_of, + phone, + postal_code, + regex, + required, + required_if, + required_with, + string, + strong, + timezone, + truthy, + uuid, + video, + when, + ) + + def register(self, *cls): + for obj in cls: + self.registry.update({obj.__name__: obj}) diff --git a/src/masonite/validation/__init__.py b/src/masonite/validation/__init__.py new file mode 100644 index 000000000..1b907c265 --- /dev/null +++ b/src/masonite/validation/__init__.py @@ -0,0 +1,48 @@ +from .RuleEnclosure import RuleEnclosure +from .MessageBag import MessageBag +from .Validator import ( + BaseValidation, + ValidationFactory, + Validator, + accepted, + active_domain, + after_today, + before_today, + confirmed, + contains, + date, + different, + distinct, + does_not, + email, + equals, + exists, + file, + greater_than, + image, + in_range, + ip, + is_future, + is_list, + is_in, + is_past, + isnt, + json, + length, + less_than, + none, + numeric, + phone, + postal_code, + regex, + required, + required_if, + required_with, + string, + strong, + timezone, + truthy, + uuid, + video, + when, +) diff --git a/src/masonite/validation/commands/MakeRuleCommand.py b/src/masonite/validation/commands/MakeRuleCommand.py new file mode 100644 index 000000000..52e652756 --- /dev/null +++ b/src/masonite/validation/commands/MakeRuleCommand.py @@ -0,0 +1,46 @@ +"""New Rule Command.""" +from cleo import Command +import inflection +import os + +from ...utils.filesystem import get_module_dir, make_directory, render_stub_file +from ...utils.location import base_path +from ...utils.str import as_filepath + + +class MakeRuleCommand(Command): + """ + Creates a new rule. + + rule + {name : Name of the rule} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + + content = render_stub_file(self.get_stub_rule_path(), name) + + relative_filename = os.path.join( + as_filepath(self.app.make("validation.location")), name + ".py" + ) + + if os.path.exists(relative_filename): + return self.line( + f"+ + {% if type(getattr(obj, key)) in show_methods %} + + {% if key.startswith('__') %} + - + {% elif key.startswith('_') %} + # + {% else %} + + + {% endif %} + {{ key }} + - + {% if not hasattr(property, '__self__') %} + {{ type(getattr(obj, key)).__name__ }} + {% if type(getattr(obj, key)) == dict %} + : {{ len(getattr(obj, key)) }} + {% if len(getattr(obj, key)) %} ++ +
+ {% for key, value in getattr(obj, key).items() %} +{{ key }}: {{ value }}+ {% endfor %} + {% endif %} + {% elif type(getattr(obj, key)) == list %} + : {{ len(getattr(obj, key)) }} + {% if len(getattr(obj, key)) %} +
+ {% for key in getattr(obj, key) %} + {% if hasattr(key, 'serialize') %} +{{ key.serialize() }}+ {% endif %} + {% endfor %} + {% endif %} + {% else %} + {{ property }} + {% endif %} + {% endif %} + {% endif %} + +File ({relative_filename}) already exists " + ) + + filepath = base_path(relative_filename) + make_directory(filepath) + + with open(filepath, "w") as f: + f.write(content) + + self.info(f"Validation Rule Created ({relative_filename})") + + def get_stub_rule_path(self): + return os.path.join(get_module_dir(__file__), "../../stubs/validation/Rule.py") diff --git a/src/masonite/validation/commands/MakeRuleEnclosureCommand.py b/src/masonite/validation/commands/MakeRuleEnclosureCommand.py new file mode 100644 index 000000000..acc101755 --- /dev/null +++ b/src/masonite/validation/commands/MakeRuleEnclosureCommand.py @@ -0,0 +1,46 @@ +"""New Rule Enclosure Command.""" +from cleo import Command +import inflection +import os + +from ...utils.filesystem import get_module_dir, make_directory, render_stub_file +from ...utils.location import base_path +from ...utils.str import as_filepath + + +class MakeRuleEnclosureCommand(Command): + """ + Creates a new rule enclosure. + + rule:enclosure + {name : Name of the rule enclosure} + """ + + def __init__(self, application): + super().__init__() + self.app = application + + def handle(self): + name = inflection.camelize(self.argument("name")) + content = render_stub_file(self.get_stub_rule_enclosure_path(), name) + + relative_filename = os.path.join( + as_filepath(self.app.make("validation.location")), name + ".py" + ) + + if os.path.exists(relative_filename): + return self.line( + f"File ({relative_filename}) already exists " + ) + filepath = base_path(relative_filename) + make_directory(filepath) + + with open(filepath, "w") as f: + f.write(content) + + self.info(f"Validation Created ({relative_filename})") + + def get_stub_rule_enclosure_path(self): + return os.path.join( + get_module_dir(__file__), "../../stubs/validation/RuleEnclosure.py" + ) diff --git a/storage/__init__.py b/src/masonite/validation/commands/__init__.py similarity index 100% rename from storage/__init__.py rename to src/masonite/validation/commands/__init__.py diff --git a/src/masonite/validation/decorators.py b/src/masonite/validation/decorators.py new file mode 100644 index 000000000..26dc2702f --- /dev/null +++ b/src/masonite/validation/decorators.py @@ -0,0 +1,20 @@ +def validate(*rules, redirect=None, back=None): + def decorator(func, rules=rules): + def wrapper(*args, **kwargs): + from wsgi import container + + request = container.make("Request") + response = container.make("Response") + errors = request.validate(*rules) + if errors: + if redirect: + return response.redirect(redirect).with_errors(errors).with_input() + if back: + return response.back().with_errors(errors).with_input() + return errors + else: + return container.resolve(func) + + return wrapper + + return decorator diff --git a/src/masonite/validation/providers/ValidationProvider.py b/src/masonite/validation/providers/ValidationProvider.py new file mode 100644 index 000000000..1f35f905a --- /dev/null +++ b/src/masonite/validation/providers/ValidationProvider.py @@ -0,0 +1,35 @@ +"""A Validation Service Provider.""" +from ...providers import Provider +from .. import Validator, ValidationFactory, MessageBag +from ..commands.MakeRuleEnclosureCommand import MakeRuleEnclosureCommand +from ..commands.MakeRuleCommand import MakeRuleCommand + + +class ValidationProvider(Provider): + def __init__(self, application): + self.application = application + + def register(self): + validator = Validator() + self.application.bind("validator", validator) + self.application.make("commands").add( + MakeRuleEnclosureCommand(self.application), + MakeRuleCommand(self.application), + ) + + MessageBag.get_errors = self._get_errors + self.application.make("view").share({"bag": MessageBag.view_helper}) + validator.extend(ValidationFactory().registry) + + def boot(self): + pass + + def _get_errors(self): + request = self.application.make("request") + messages = [] + for error, message in ( + request.session.get_flashed_messages().get("errors", {}).items() + ): + messages += message + + return messages diff --git a/src/masonite/validation/providers/__init__.py b/src/masonite/validation/providers/__init__.py new file mode 100644 index 000000000..2c972a335 --- /dev/null +++ b/src/masonite/validation/providers/__init__.py @@ -0,0 +1 @@ +from .ValidationProvider import ValidationProvider diff --git a/src/masonite/validation/resources/postal_codes.py b/src/masonite/validation/resources/postal_codes.py new file mode 100644 index 000000000..79380a487 --- /dev/null +++ b/src/masonite/validation/resources/postal_codes.py @@ -0,0 +1,1011 @@ +# noqa: W605 +PATTERNS = { + "AC": { + "example": "ASCN 1ZZ", + "pattern": r"^(?:ASCN 1ZZ)$", + }, + "AD": { + "example": "AD100", + "pattern": r"^(?:AD[1-7]0\d)$", + }, + "AE": { + "example": None, + "pattern": None, + }, + "AF": { + "example": "1001", + "pattern": r"^(?:\d{4})$", + }, + "AG": { + "example": None, + "pattern": None, + }, + "AI": { + "example": "2640", + "pattern": r"^(?:(?:AI-)?2640)$", + }, + "AL": { + "example": "1001", + "pattern": r"^(?:\d{4})$", + }, + "AM": { + "example": "375010", + "pattern": r"^(?:(?:37)?\d{4})$", + }, + "AO": { + "example": None, + "pattern": None, + }, + "AQ": { + "example": None, + "pattern": None, + }, + "AR": { + "example": "C1070AAM", + "pattern": r"^(?:((?:[A-HJ-NP-Z])?\d{4})([A-Z]{3})?)$", + }, + "AS": { + "example": "96799", + "pattern": r"^(?:(96799)(?:[ \-](\d{4}))?)$", + }, + "AT": { + "example": "1010", + "pattern": r"^(?:\d{4})$", + }, + "AU": { + "example": "2060", + "pattern": r"^(?:\d{4})$", + }, + "AW": { + "example": None, + "pattern": None, + }, + "AX": { + "example": "22150", + "pattern": r"^(?:22\d{3})$", + }, + "AZ": { + "example": "1000", + "pattern": r"^(?:\d{4})$", + }, + "BA": { + "example": "71000", + "pattern": r"^(?:\d{5})$", + }, + "BB": { + "example": "BB23026", + "pattern": r"^(?:BB\d{5})$", + }, + "BD": { + "example": "1340", + "pattern": r"^(?:\d{4})$", + }, + "BE": { + "example": "4000", + "pattern": r"^(?:\d{4})$", + }, + "BF": { + "example": None, + "pattern": None, + }, + "BG": { + "example": "1000", + "pattern": r"^(?:\d{4})$", + }, + "BH": { + "example": "317", + "pattern": r"^(?:(?:\d|1[0-2])\d{2})$", + }, + "BI": { + "example": None, + "pattern": None, + }, + "BJ": { + "example": None, + "pattern": None, + }, + "BL": { + "example": "97100", + "pattern": r"^(?:9[78][01]\d{2})$", + }, + "BM": { + "example": "FL 07", + "pattern": r"^(?:[A-Z]{2} ?[A-Z0-9]{2})$", + }, + "BN": { + "example": "BT2328", + "pattern": r"^(?:[A-Z]{2} ?\d{4})$", + }, + "BO": { + "example": None, + "pattern": None, + }, + "BQ": { + "example": None, + "pattern": None, + }, + "BR": { + "example": "40301-110", + "pattern": r"^(?:\d{5}-?\d{3})$", + }, + "BS": { + "example": None, + "pattern": None, + }, + "BT": { + "example": "11001", + "pattern": r"^(?:\d{5})$", + }, + "BV": { + "example": None, + "pattern": None, + }, + "BW": { + "example": None, + "pattern": None, + }, + "BY": { + "example": "223016", + "pattern": r"^(?:\d{6})$", + }, + "BZ": { + "example": None, + "pattern": None, + }, + "CA": { + "example": "H3Z 2Y7", + "pattern": r"^(?:[ABCEGHJKLMNPRSTVXY]\d[ABCEGHJ-NPRSTV-Z] ?\d[ABCEGHJ-NPRSTV-Z]\d)$", + }, + "CC": { + "example": "6799", + "pattern": r"^(?:6799)$", + }, + "CD": { + "example": None, + "pattern": None, + }, + "CF": { + "example": None, + "pattern": None, + }, + "CG": { + "example": None, + "pattern": None, + }, + "CH": { + "example": "2544", + "pattern": r"^(?:\d{4})$", + }, + "CI": { + "example": None, + "pattern": None, + }, + "CK": { + "example": None, + "pattern": None, + }, + "CL": { + "example": "8340457", + "pattern": r"^(?:\d{7})$", + }, + "CM": { + "example": None, + "pattern": None, + }, + "CN": { + "example": "266033", + "pattern": r"^(?:\d{6})$", + }, + "CO": { + "example": "111221", + "pattern": r"^(?:\d{6})$", + }, + "CR": { + "example": "1000", + "pattern": r"^(?:\d{4,5}|\d{3}-\d{4})$", + }, + "CU": { + "example": "10700", + "pattern": r"^(?:\d{5})$", + }, + "CV": { + "example": "7600", + "pattern": r"^(?:\d{4})$", + }, + "CW": { + "example": None, + "pattern": None, + }, + "CX": { + "example": "6798", + "pattern": r"^(?:6798)$", + }, + "CY": { + "example": "2008", + "pattern": r"^(?:\d{4})$", + }, + "CZ": { + "example": "100 00", + "pattern": r"^(?:\d{3} ?\d{2})$", + }, + "DE": { + "example": "26133", + "pattern": r"^(?:\d{5})$", + }, + "DJ": { + "example": None, + "pattern": None, + }, + "DK": { + "example": "8660", + "pattern": r"^(?:\d{4})$", + }, + "DM": { + "example": None, + "pattern": None, + }, + "DO": { + "example": "11903", + "pattern": r"^(?:\d{5})$", + }, + "DZ": { + "example": "40304", + "pattern": r"^(?:\d{5})$", + }, + "EC": { + "example": "090105", + "pattern": r"^(?:\d{6})$", + }, + "EE": { + "example": "69501", + "pattern": r"^(?:\d{5})$", + }, + "EG": { + "example": "12411", + "pattern": r"^(?:\d{5})$", + }, + "EH": { + "example": "70000", + "pattern": r"^(?:\d{5})$", + }, + "ER": { + "example": None, + "pattern": None, + }, + "ES": { + "example": "28039", + "pattern": r"^(?:\d{5})$", + }, + "ET": { + "example": "1000", + "pattern": r"^(?:\d{4})$", + }, + "FI": { + "example": "00550", + "pattern": r"^(?:\d{5})$", + }, + "FJ": { + "example": None, + "pattern": None, + }, + "FK": { + "example": "FIQQ 1ZZ", + "pattern": r"^(?:FIQQ 1ZZ)$", + }, + "FM": { + "example": "96941", + "pattern": r"^(?:(9694[1-4])(?:[ \-](\d{4}))?)$", + }, + "FO": { + "example": "100", + "pattern": r"^(?:\d{3})$", + }, + "FR": { + "example": "33380", + "pattern": r"^(?:\d{2} ?\d{3})$", + }, + "GA": { + "example": None, + "pattern": None, + }, + "GB": { + "example": "EC1Y 8SY", + "pattern": r"^(?:GIR ?0AA|(?:(?:AB|AL|B|BA|BB|BD|BF|BH|BL|BN|BR|BS|BT|BX|CA|CB|CF|CH|CM|CO|CR|CT|CV|CW|DA|DD|DE|DG|DH|DL|DN|DT|DY|E|EC|EH|EN|EX|FK|FY|G|GL|GY|GU|HA|HD|HG|HP|HR|HS|HU|HX|IG|IM|IP|IV|JE|KA|KT|KW|KY|L|LA|LD|LE|LL|LN|LS|LU|M|ME|MK|ML|N|NE|NG|NN|NP|NR|NW|OL|OX|PA|PE|PH|PL|PO|PR|RG|RH|RM|S|SA|SE|SG|SK|SL|SM|SN|SO|SP|SR|SS|ST|SW|SY|TA|TD|TF|TN|TQ|TR|TS|TW|UB|W|WA|WC|WD|WF|WN|WR|WS|WV|YO|ZE)(?:\d[\dA-Z]? ?\d[ABD-HJLN-UW-Z]{2}))|BFPO ?\d{1,4})$", + }, + "GD": { + "example": None, + "pattern": None, + }, + "GE": { + "example": "0101", + "pattern": r"^(?:\d{4})$", + }, + "GF": { + "example": "97300", + "pattern": r"^(?:9[78]3\d{2})$", + }, + "GG": { + "example": "GY1 1AA", + "pattern": r"^(?:GY\d[\dA-Z]? ?\d[ABD-HJLN-UW-Z]{2})$", + }, + "GH": { + "example": None, + "pattern": None, + }, + "GI": { + "example": "GX11 1AA", + "pattern": r"^(?:GX11 1AA)$", + }, + "GL": { + "example": "3900", + "pattern": r"^(?:39\d{2})$", + }, + "GM": { + "example": None, + "pattern": None, + }, + "GN": { + "example": "001", + "pattern": r"^(?:\d{3})$", + }, + "GP": { + "example": "97100", + "pattern": r"^(?:9[78][01]\d{2})$", + }, + "GQ": { + "example": None, + "pattern": None, + }, + "GR": { + "example": "151 24", + "pattern": r"^(?:\d{3} ?\d{2})$", + }, + "GS": { + "example": "SIQQ 1ZZ", + "pattern": r"^(?:SIQQ 1ZZ)$", + }, + "GT": { + "example": "09001", + "pattern": r"^(?:\d{5})$", + }, + "GU": { + "example": "96910", + "pattern": r"^(?:(969(?:[12]\d|3[12]))(?:[ \-](\d{4}))?)$", + }, + "GW": { + "example": "1000", + "pattern": r"^(?:\d{4})$", + }, + "GY": { + "example": None, + "pattern": None, + }, + "HK": { + "example": None, + "pattern": None, + }, + "HM": { + "example": "7050", + "pattern": r"^(?:\d{4})$", + }, + "HN": { + "example": "31301", + "pattern": r"^(?:\d{5})$", + }, + "HR": { + "example": "10000", + "pattern": r"^(?:\d{5})$", + }, + "HT": { + "example": "6120", + "pattern": r"^(?:\d{4})$", + }, + "HU": { + "example": "1037", + "pattern": r"^(?:\d{4})$", + }, + "ID": { + "example": "40115", + "pattern": r"^(?:\d{5})$", + }, + "IE": { + "example": "A65 F4E2", + "pattern": r"^(?:[\dA-Z]{3} ?[\dA-Z]{4})$", + }, + "IL": { + "example": "9614303", + "pattern": r"^(?:\d{5}(?:\d{2})?)$", + }, + "IM": { + "example": "IM2 1AA", + "pattern": r"^(?:IM\d[\dA-Z]? ?\d[ABD-HJLN-UW-Z]{2})$", + }, + "IN": { + "example": "110034", + "pattern": r"^(?:\d{6})$", + }, + "IO": { + "example": "BBND 1ZZ", + "pattern": r"^(?:BBND 1ZZ)$", + }, + "IQ": { + "example": "31001", + "pattern": r"^(?:\d{5})$", + }, + "IR": { + "example": "11936-12345", + "pattern": r"^(?:\d{5}-?\d{5})$", + }, + "IS": { + "example": "320", + "pattern": r"^(?:\d{3})$", + }, + "IT": { + "example": "00144", + "pattern": r"^(?:\d{5})$", + }, + "JE": { + "example": "JE1 1AA", + "pattern": r"^(?:JE\d[\dA-Z]? ?\d[ABD-HJLN-UW-Z]{2})$", + }, + "JM": { + "example": None, + "pattern": None, + }, + "JO": { + "example": "11937", + "pattern": r"^(?:\d{5})$", + }, + "JP": { + "example": "154-0023", + "pattern": r"^(?:\d{3}-?\d{4})$", + }, + "KE": { + "example": "20100", + "pattern": r"^(?:\d{5})$", + }, + "KG": { + "example": "720001", + "pattern": r"^(?:\d{6})$", + }, + "KH": { + "example": "12203", + "pattern": r"^(?:\d{5})$", + }, + "KI": { + "example": None, + "pattern": None, + }, + "KM": { + "example": None, + "pattern": None, + }, + "KN": { + "example": None, + "pattern": None, + }, + "KP": { + "example": None, + "pattern": None, + }, + "KR": { + "example": "03051", + "pattern": r"^(?:\d{5})$", + }, + "KW": { + "example": "54541", + "pattern": r"^(?:\d{5})$", + }, + "KY": { + "example": "KY1-1100", + "pattern": r"^(?:KY\d-\d{4})$", + }, + "KZ": { + "example": "040900", + "pattern": r"^(?:\d{6})$", + }, + "LA": { + "example": "01160", + "pattern": r"^(?:\d{5})$", + }, + "LB": { + "example": "2038 3054", + "pattern": r"^(?:(?:\d{4})(?: ?(?:\d{4}))?)$", + }, + "LC": { + "example": None, + "pattern": None, + }, + "LI": { + "example": "9496", + "pattern": r"^(?:948[5-9]|949[0-8])$", + }, + "LK": { + "example": "20000", + "pattern": r"^(?:\d{5})$", + }, + "LR": { + "example": "1000", + "pattern": r"^(?:\d{4})$", + }, + "LS": { + "example": "100", + "pattern": r"^(?:\d{3})$", + }, + "LT": { + "example": "04340", + "pattern": r"^(?:\d{5})$", + }, + "LU": { + "example": "4750", + "pattern": r"^(?:\d{4})$", + }, + "LV": { + "example": "LV-1073", + "pattern": r"^(?:LV-\d{4})$", + }, + "LY": { + "example": None, + "pattern": None, + }, + "MA": { + "example": "53000", + "pattern": r"^(?:\d{5})$", + }, + "MC": { + "example": "98000", + "pattern": r"^(?:980\d{2})$", + }, + "MD": { + "example": "2012", + "pattern": r"^(?:\d{4})$", + }, + "ME": { + "example": "81257", + "pattern": r"^(?:8\d{4})$", + }, + "MF": { + "example": "97100", + "pattern": r"^(?:9[78][01]\d{2})$", + }, + "MG": { + "example": "501", + "pattern": r"^(?:\d{3})$", + }, + "MH": { + "example": "96960", + "pattern": r"^(?:(969[67]\d)(?:[ \-](\d{4}))?)$", + }, + "MK": { + "example": "1314", + "pattern": r"^(?:\d{4})$", + }, + "ML": { + "example": None, + "pattern": None, + }, + "MM": { + "example": "11181", + "pattern": r"^(?:\d{5})$", + }, + "MN": { + "example": "65030", + "pattern": r"^(?:\d{5})$", + }, + "MO": { + "example": None, + "pattern": None, + }, + "MP": { + "example": "96950", + "pattern": r"^(?:(9695[012])(?:[ \-](\d{4}))?)$", + }, + "MQ": { + "example": "97220", + "pattern": r"^(?:9[78]2\d{2})$", + }, + "MR": { + "example": None, + "pattern": None, + }, + "MS": { + "example": None, + "pattern": None, + }, + "MT": { + "example": "NXR 01", + "pattern": r"^(?:[A-Z]{3} ?\d{2,4})$", + }, + "MU": { + "example": "42602", + "pattern": r"^(?:\d{3}(?:\d{2}|[A-Z]{2}\d{3}))$", + }, + "MV": { + "example": "20026", + "pattern": r"^(?:\d{5})$", + }, + "MW": { + "example": None, + "pattern": None, + }, + "MX": { + "example": "02860", + "pattern": r"^(?:\d{5})$", + }, + "MY": { + "example": "43000", + "pattern": r"^(?:\d{5})$", + }, + "MZ": { + "example": "1102", + "pattern": r"^(?:\d{4})$", + }, + "NA": { + "example": "10001", + "pattern": r"^(?:\d{5})$", + }, + "NC": { + "example": "98814", + "pattern": r"^(?:988\d{2})$", + }, + "NE": { + "example": "8001", + "pattern": r"^(?:\d{4})$", + }, + "NF": { + "example": "2899", + "pattern": r"^(?:2899)$", + }, + "NG": { + "example": "930283", + "pattern": r"^(?:\d{6})$", + }, + "NI": { + "example": "52000", + "pattern": r"^(?:\d{5})$", + }, + "NL": { + "example": "1234 AB", + "pattern": r"^(?:\d{4} ?[A-Z]{2})$", + }, + "NO": { + "example": "0025", + "pattern": r"^(?:\d{4})$", + }, + "NP": { + "example": "44601", + "pattern": r"^(?:\d{5})$", + }, + "NR": { + "example": None, + "pattern": None, + }, + "NU": { + "example": None, + "pattern": None, + }, + "NZ": { + "example": "6001", + "pattern": r"^(?:\d{4})$", + }, + "OM": { + "example": "133", + "pattern": r"^(?:(?:PC )?\d{3})$", + }, + "PA": { + "example": None, + "pattern": None, + }, + "PE": { + "example": "LIMA 23", + "pattern": r"^(?:(?:LIMA \d{1,2}|CALLAO 0?\d)|[0-2]\d{4})$", + }, + "PF": { + "example": "98709", + "pattern": r"^(?:987\d{2})$", + }, + "PG": { + "example": "111", + "pattern": r"^(?:\d{3})$", + }, + "PH": { + "example": "1008", + "pattern": r"^(?:\d{4})$", + }, + "PK": { + "example": "44000", + "pattern": r"^(?:\d{5})$", + }, + "PL": { + "example": "00-950", + "pattern": r"^(?:\d{2}-\d{3})$", + }, + "PM": { + "example": "97500", + "pattern": r"^(?:9[78]5\d{2})$", + }, + "PN": { + "example": "PCRN 1ZZ", + "pattern": r"^(?:PCRN 1ZZ)$", + }, + "PR": { + "example": "00930", + "pattern": r"^(?:(00[679]\d{2})(?:[ \-](\d{4}))?)$", + }, + "PS": { + "example": None, + "pattern": None, + }, + "PT": { + "example": "2725-079", + "pattern": r"^(?:\d{4}-\d{3})$", + }, + "PW": { + "example": "96940", + "pattern": r"^(?:(969(?:39|40))(?:[ \-](\d{4}))?)$", + }, + "PY": { + "example": "1536", + "pattern": r"^(?:\d{4})$", + }, + "QA": { + "example": None, + "pattern": None, + }, + "RE": { + "example": "97400", + "pattern": r"^(?:9[78]4\d{2})$", + }, + "RO": { + "example": "060274", + "pattern": r"^(?:\d{6})$", + }, + "RS": { + "example": "106314", + "pattern": r"^(?:\d{5,6})$", + }, + "RU": { + "example": "247112", + "pattern": r"^(?:\d{6})$", + }, + "RW": { + "example": None, + "pattern": None, + }, + "SA": { + "example": "11564", + "pattern": r"^(?:\d{5})$", + }, + "SB": { + "example": None, + "pattern": None, + }, + "SC": { + "example": None, + "pattern": None, + }, + "SD": { + "example": "11042", + "pattern": r"^(?:\d{5})$", + }, + "SE": { + "example": "11455", + "pattern": r"^(?:\d{3} ?\d{2})$", + }, + "SG": { + "example": "546080", + "pattern": r"^(?:\d{6})$", + }, + "SH": { + "example": "STHL 1ZZ", + "pattern": r"^(?:(?:ASCN|STHL) 1ZZ)$", + }, + "SI": { + "example": "4000", + "pattern": r"^(?:\d{4})$", + }, + "SJ": { + "example": "9170", + "pattern": r"^(?:\d{4})$", + }, + "SK": { + "example": "010 01", + "pattern": r"^(?:\d{3} ?\d{2})$", + }, + "SL": { + "example": None, + "pattern": None, + }, + "SM": { + "example": "47890", + "pattern": r"^(?:4789\d)$", + }, + "SN": { + "example": "12500", + "pattern": r"^(?:\d{5})$", + }, + "SO": { + "example": "JH 09010", + "pattern": r"^(?:[A-Z]{2} ?\d{5})$", + }, + "SR": { + "example": None, + "pattern": None, + }, + "SS": { + "example": None, + "pattern": None, + }, + "ST": { + "example": None, + "pattern": None, + }, + "SV": { + "example": "CP 1101", + "pattern": r"^(?:CP [1-3][1-7][0-2]\d)$", + }, + "SX": { + "example": None, + "pattern": None, + }, + "SY": { + "example": None, + "pattern": None, + }, + "SZ": { + "example": "H100", + "pattern": r"^(?:[HLMS]\d{3})$", + }, + "TA": { + "example": "TDCU 1ZZ", + "pattern": r"^(?:TDCU 1ZZ)$", + }, + "TC": { + "example": "TKCA 1ZZ", + "pattern": r"^(?:TKCA 1ZZ)$", + }, + "TD": { + "example": None, + "pattern": None, + }, + "TF": { + "example": None, + "pattern": None, + }, + "TG": { + "example": None, + "pattern": None, + }, + "TH": { + "example": "10150", + "pattern": r"^(?:\d{5})$", + }, + "TJ": { + "example": "735450", + "pattern": r"^(?:\d{6})$", + }, + "TK": { + "example": None, + "pattern": None, + }, + "TL": { + "example": None, + "pattern": None, + }, + "TM": { + "example": "744000", + "pattern": r"^(?:\d{6})$", + }, + "TN": { + "example": "1002", + "pattern": r"^(?:\d{4})$", + }, + "TO": { + "example": None, + "pattern": None, + }, + "TR": { + "example": "01960", + "pattern": r"^(?:\d{5})$", + }, + "TT": { + "example": None, + "pattern": None, + }, + "TV": { + "example": None, + "pattern": None, + }, + "TW": { + "example": "104", + "pattern": r"^(?:\d{3}(?:\d{2,3})?)$", + }, + "TZ": { + "example": "6090", + "pattern": r"^(?:\d{4,5})$", + }, + "UA": { + "example": "15432", + "pattern": r"^(?:\d{5})$", + }, + "UG": { + "example": None, + "pattern": None, + }, + "UM": { + "example": "96898", + "pattern": r"^(?:96898)$", + }, + "US": { + "example": "95014", + "pattern": r"^(?:(\d{5})(?:[ \-](\d{4}))?)$", + }, + "UY": { + "example": "11600", + "pattern": r"^(?:\d{5})$", + }, + "UZ": { + "example": "702100", + "pattern": r"^(?:\d{6})$", + }, + "VA": { + "example": "00120", + "pattern": r"^(?:00120)$", + }, + "VC": { + "example": "VC0100", + "pattern": r"^(?:VC\d{4})$", + }, + "VE": { + "example": "1010", + "pattern": r"^(?:\d{4})$", + }, + "VG": { + "example": "VG1110", + "pattern": r"^(?:VG\d{4})$", + }, + "VI": { + "example": "00802-1222", + "pattern": r"^(?:(008(?:(?:[0-4]\d)|(?:5[01])))(?:[ \-](\d{4}))?)$", + }, + "VN": { + "example": "70010", + "pattern": r"^(?:\d{5}\d?)$", + }, + "VU": { + "example": None, + "pattern": None, + }, + "WF": { + "example": "98600", + "pattern": r"^(?:986\d{2})$", + }, + "WS": { + "example": None, + "pattern": None, + }, + "XK": { + "example": "10000", + "pattern": r"^(?:[1-7]\d{4})$", + }, + "YE": { + "example": None, + "pattern": None, + }, + "YT": { + "example": "97600", + "pattern": r"^(?:976\d{2})$", + }, + "ZA": { + "example": "0083", + "pattern": r"^(?:\d{4})$", + }, + "ZM": { + "example": "50100", + "pattern": r"^(?:\d{5})$", + }, + "ZW": { + "example": None, + "pattern": None, + }, +} diff --git a/src/masonite/view.py b/src/masonite/view.py deleted file mode 100644 index 04ee85fdf..000000000 --- a/src/masonite/view.py +++ /dev/null @@ -1,321 +0,0 @@ -"""View Module.""" - - -from jinja2 import ChoiceLoader, Environment, PackageLoader, select_autoescape -from jinja2.exceptions import TemplateNotFound - -from .exceptions import RequiredContainerBindingNotFound, ViewException -from .response import Responsable - - -class View(Responsable): - """View class. Responsible for handling everything involved with views and view environments.""" - - _splice = "/" - - def __init__(self, container): - """View constructor. - - Arguments: - container {masonite.app.App} -- Container object. - """ - self.dictionary = {} - self.composers = {} - self.container = container - - # If the cache_for method is declared - self.cache = False - # Cache time of cache_for - self.cache_time = None - # Cache type of cache_for - self.cache_type = None - - self.template = None - self.environments = [] - self.extension = ".html" - self._jinja_extensions = ["jinja2.ext.loopcontrols"] - self._filters = {} - self._tests = {} - self._shared = {} - - def render(self, template, dictionary={}): - """Get the string contents of the view. - - Arguments: - template {string} -- Name of the template you want to render. - - Keyword Arguments: - dictionary {dict} -- Data that you want to pass into your view. (default: {{}}) - - Returns: - self - """ - if not isinstance(dictionary, dict): - raise ViewException( - "Second parameter to render method needs to be a dictionary, {} passed.".format( - type(dictionary).__name__ - ) - ) - - self.__load_environment(template) - self.dictionary = {} - - self.dictionary.update(dictionary) - self.dictionary.update(self._shared) - - # Check if use cache and return template from cache if exists - if ( - self.container.has("Cache") - and self.__cached_template_exists() - and not self.__is_expired_cache() - ): - return self.__get_cached_template() - - # Check if composers are even set for a speed improvement - if self.composers: - self._update_from_composers() - - if self._tests: - self.env.tests.update(self._tests) - - self.rendered_template = self._render() - - return self - - def _render(self): - try: - # Try rendering the template with '.html' appended - return self.env.get_template(self.filename).render(self.dictionary) - except TemplateNotFound: - # Try rendering the direct template the user has supplied - return self.env.get_template(self.template).render(self.dictionary) - - def _update_from_composers(self): - """Add data into the view from specified composers.""" - # Check if the template is directly specified in the composer - if self.template in self.composers: - self.dictionary.update(self.composers.get(self.template)) - - # Check if there is just an astericks in the composer - if "*" in self.composers: - self.dictionary.update(self.composers.get("*")) - - # We will append onto this string for an easier way to search through wildcard routes - compiled_string = "" - - # Check for wildcard view composers - for template in self.template.split(self._splice): - # Append the template onto the compiled_string - compiled_string += template - if self.composers.get("{}*".format(compiled_string)): - self.dictionary.update(self.composers["{}*".format(compiled_string)]) - else: - # Add a slash to symbolize going into a deeper directory structure - compiled_string += "/" - - def composer(self, composer_name, dictionary): - """Update composer dictionary. - - Arguments: - composer_name {string} -- Key to bind dictionary of data to. - dictionary {dict} -- Dictionary of data to add to controller. - - Returns: - self - """ - if isinstance(composer_name, str): - self.composers[composer_name] = dictionary - - if isinstance(composer_name, list): - for composer in composer_name: - self.composers[composer] = dictionary - - return self - - def share(self, dictionary): - """Share data to all templates. - - Arguments: - dictionary {dict} -- Dictionary of key value pairs to add to all views. - - Returns: - self - """ - self._shared.update(dictionary) - return self - - def cache_for(self, time=None, cache_type=None): - """Set time and type for cache. - - Keyword Arguments: - time {string} -- Time to cache template for (default: {None}) - cache_type {string} -- Type of the cache. (default: {None}) - - Raises: - RequiredContainerBindingNotFound -- Thrown when the Cache key binding is not found in the container. - - Returns: - self - """ - if not self.container.has("Cache"): - raise RequiredContainerBindingNotFound( - "The 'Cache' container binding is required to use this method and wasn't found in the container. You may be missing a Service Provider" - ) - - self.cache = True - self.cache_time = float(time) - self.cache_type = cache_type - if self.__is_expired_cache(): - self.__create_cache_template(self.template) - return self - - def exists(self, template): - """Check if a template exists. - - Arguments: - template {string} -- Name of the template to check for. - - Returns: - bool - """ - self.__load_environment(template) - - try: - self.env.get_template(self.filename) - return True - except TemplateNotFound: - return False - - def add_environment(self, template_location, loader=PackageLoader): - """Add an environment to the templates. - - Arguments: - template_location {string} -- Directory location to attach the environment to. - - Keyword Arguments: - loader {jinja2.Loader} -- Type of Jinja2 loader to use. (default: {jinja2.PackageLoader}) - """ - if loader == PackageLoader: - template_location = template_location.split(self._splice) - - self.environments.append( - loader(template_location[0], "/".join(template_location[1:])) - ) - else: - self.environments.append(loader(template_location)) - - def filter(self, name, function): - """Use to add filters to views. - - Arguments: - name {string} -- Key to bind the filter to. - function {object} -- Function used for the template filter. - """ - self._filters.update({name: function}) - - def test(self, key, obj): - self._tests.update({key: obj}) - return self - - def add_extension(self, extension): - self._jinja_extensions.append(extension) - return self - - def __load_environment(self, template): - """Private method for loading all the environments. - - Arguments: - template {string} -- Template to load environment from. - """ - self.template = template - self.filename = ( - template.replace(self._splice, "/").replace(".", "/") + self.extension - ) - - if template.startswith("/"): - # Filter blanks strings from the split - location = list(filter(None, template.split("/"))) - self.filename = location[-1] + self.extension - - loader = ChoiceLoader( - [PackageLoader(location[0], "/".join(location[1:-1]))] - + self.environments - ) - self.env = Environment( - loader=loader, - autoescape=select_autoescape(["html", "xml"]), - extensions=self._jinja_extensions, - line_statement_prefix="@", - ) - - else: - loader = ChoiceLoader( - [PackageLoader("resources", "templates")] + self.environments - ) - - # Set the searchpath since some packages look for this object - # This is sort of a hack for now - loader.searchpath = "" - - self.env = Environment( - loader=loader, - autoescape=select_autoescape(["html", "xml"]), - extensions=self._jinja_extensions, - line_statement_prefix="@", - ) - - self.env.filters.update(self._filters) - - def __create_cache_template(self, template): - """Save in the cache the template. - - Arguments: - template {string} -- Creates the cached templates. - """ - self.container.make("Cache").store_for( - template, - self.rendered_template, - self.cache_time, - self.cache_type, - ".html", - ) - - def __cached_template_exists(self): - """Check if the cache template exists. - - Returns: - bool - """ - return self.container.make("Cache").exists(self.template) - - def __is_expired_cache(self): - """Check if cache is expired. - - Returns: - bool - """ - # Check if cache_for is set and configurate - if self.cache_time is None or self.cache_type is None and self.cache: - return True - - driver_cache = self.container.make("Cache") - - # True is expired - return not driver_cache.is_valid(self.template) - - def __get_cached_template(self): - """Return the cached version of the template. - - Returns: - self - """ - driver_cache = self.container.make("Cache") - self.rendered_template = driver_cache.get(self.template) - return self - - def set_splice(self, splice): - self._splice = splice - return self - - def get_response(self): - return self.rendered_template diff --git a/bootstrap/cache/.gitignore b/src/masonite/views/ViewCapsule.py similarity index 100% rename from bootstrap/cache/.gitignore rename to src/masonite/views/ViewCapsule.py diff --git a/src/masonite/views/__init__.py b/src/masonite/views/__init__.py new file mode 100644 index 000000000..7c633a131 --- /dev/null +++ b/src/masonite/views/__init__.py @@ -0,0 +1 @@ +from .view import View diff --git a/src/masonite/views/view.py b/src/masonite/views/view.py new file mode 100644 index 000000000..d613b1d09 --- /dev/null +++ b/src/masonite/views/view.py @@ -0,0 +1,275 @@ +"""View Module.""" +from collections import defaultdict +from os.path import split, exists +from jinja2 import ChoiceLoader, Environment, PackageLoader, select_autoescape +from jinja2.exceptions import TemplateNotFound + +from ..exceptions import ViewException +from ..utils.str import as_filepath +from ..utils.location import views_path + + +def path_to_package(path, separator="/"): + # ensure no leading/trailing slashes before splitting to avoid blank strings + location = path.strip(separator).split(separator) + package_name = location[0] + package_path = "/".join(location[1:]) + return package_name, package_path + + +class View: + """Responsible for handling everything involved with views and view environments.""" + + separator = "/" + extension = ".html" + + def __init__(self, application): + self.application = application + + # specific to given view rendering + self.dictionary = {} + self.composers = {} + self.template = None + self.loaders = [] + self.namespaces = defaultdict(list) + self.env = None + self._jinja_extensions = ["jinja2.ext.loopcontrols"] + self._filters = {} + self._shared = {} + self._tests = {} + + def render(self, template, dictionary={}): + """Get the string contents of the view. + + Arguments: + template {string} -- Name of the template you want to render. + + Keyword Arguments: + dictionary {dict} -- Data that you want to pass into your view. (default: {{}}) + + Returns: + self + """ + if not isinstance(dictionary, dict): + raise ViewException( + "Second parameter to render method needs to be a dictionary, {} passed.".format( + type(dictionary).__name__ + ) + ) + + self.load_template(template) + + # prepare template context + self.dictionary = {} + self.dictionary.update(dictionary) + self.dictionary.update(self._shared) + if self.composers: + self.hydrate_from_composers() + + if self._tests: + self.env.tests.update(self._tests) + + self.rendered_template = self._render() + + return self + + def get_content(self): + return self.rendered_template + + def _render(self): + try: + # Try rendering the template with '.html' appended + return self.env.get_template(self.filename).render(self.dictionary) + except TemplateNotFound: + # Try rendering the direct template the user has supplied + return self.env.get_template(self.template).render(self.dictionary) + + def hydrate_from_composers(self): + """Add data into the view from specified composers.""" + # Check if the template is directly specified in the composer + if self.template in self.composers: + self.dictionary.update(self.composers.get(self.template)) + + # Check if there is just an astericks in the composer + if "*" in self.composers: + self.dictionary.update(self.composers.get("*")) + + # We will append onto this string for an easier way to search through wildcard routes + compiled_string = "" + + # Check for wildcard view composers + for template in self.template.split(self.separator): + # Append the template onto the compiled_string + compiled_string += template + if self.composers.get("{}*".format(compiled_string)): + self.dictionary.update(self.composers["{}*".format(compiled_string)]) + else: + # Add a slash to symbolize going into a deeper directory structure + compiled_string += "/" + + def composer(self, composer_name, dictionary): + """Update composer dictionary. + + Arguments: + composer_name {string} -- Key to bind dictionary of data to. + dictionary {dict} -- Dictionary of data to add to controller. + + Returns: + self + """ + if isinstance(composer_name, str): + self.composers[composer_name] = dictionary + + if isinstance(composer_name, list): + for composer in composer_name: + self.composers[composer] = dictionary + + return self + + def share(self, dictionary): + """Share data to all templates. + + Arguments: + dictionary {dict} -- Dictionary of key value pairs to add to all views. + + Returns: + self + """ + self._shared.update(dictionary) + return self + + def exists(self, template): + """Check if a template exists. + + Arguments: + template {string} -- Name of the template to check for. + + Returns: + bool + """ + self.load_template(template) + + try: + self.env.get_template(self.filename) + return True + except TemplateNotFound: + return False + + def add_location(self, template_location, loader=PackageLoader): + """Add locations from which view templates can be loaded. + + Arguments: + template_location {str} -- Directory location + + Keyword Arguments: + loader {jinja2.Loader} -- Type of Jinja2 loader to use. (default: {jinja2.PackageLoader}) + """ + if loader == PackageLoader: + package_name, package_path = path_to_package(template_location) + self.loaders.append(loader(package_name, package_path)) + else: + self.loaders.append(loader(template_location)) + + def add_namespaced_location(self, namespace, template_location): + # if views have been published, add the published view directory as a location + published_path = views_path(f"vendor/{namespace}/", absolute=False) + if exists(published_path): + self.namespaces[namespace].append( + views_path(f"vendor/{namespace}/", absolute=False) + ) + # put this one in 2nd as project views must be used first to be able to override package views + self.namespaces[namespace].append(template_location) + + def add_from_package(self, package_name, path_in_package): + self.environments.append(PackageLoader(package_name, path_in_package)) + + def add_namespace(self, namespace, path): + # TODO: if views have been published, add an other path corresponding to this namespace + self.namespaces[namespace].append( + views_path(f"vendor/{namespace}/", absolute=False) + ) + # put this one in 2nd as project (overriden) views must be used first + self.namespaces[namespace].append(path) + + def filter(self, name, function): + """Use to add filters to views. + + Arguments: + name {string} -- Key to bind the filter to. + function {object} -- Function used for the template filter. + """ + self._filters.update({name: function}) + + def add_extension(self, extension): + self._jinja_extensions.append(extension) + return self + + def load_template(self, template): + """Private method for loading all the locations into the current environment. + + Arguments: + template {string} -- Template to load environment from. + """ + self.template = template + # transform given template path into a real file path with the configured extension + self.filename = ( + as_filepath(template).replace(self.extension, "") + self.extension + ) + # assess if new loaders are required for the given template + template_loaders = [] + # Case 1: the templates needs to be loaded from a namespace + if ":" in template: + namespace, rel_template_path = template.split(":") + self.filename = ( + as_filepath(rel_template_path).replace(self.extension, "") + + self.extension + ) + namespace_paths = self.namespaces.get(namespace, None) + if not namespace_paths: + raise Exception(f"No such view namespace {namespace}.") + for namespace_path in namespace_paths: + package_name, package_path = path_to_package(namespace_path) + template_loaders.append(PackageLoader(package_name, package_path)) + + # Case 2: an absolute path has been given + elif template.startswith("/"): + directory, filename = split(template) + self.filename = filename.replace(self.extension, "") + self.extension + package_name, package_path = path_to_package(directory) + template_loaders.append(PackageLoader(package_name, package_path)) + + # Else: use already defined view locations to load this template + loader = ChoiceLoader(template_loaders + self.loaders) + + # @josephmancuso: what is this ?? + # Set the searchpath since some packages look for this object + # This is sort of a hack for now + loader.searchpath = "" + + self.env = Environment( + loader=loader, + autoescape=select_autoescape(["html", "xml"]), + extensions=self._jinja_extensions, + line_statement_prefix="@", + ) + # add filters to environment + self.env.filters.update(self._filters) + + def get_current_loaders(self): + if self.env: + return self.env.loader.loaders + + def set_separator(self, token): + self.separator = token + return self + + def set_file_extension(self, extension): + self.extension = extension + return self + + def get_response(self): + return self.rendered_template + + def test(self, key, obj): + self._tests.update({key: obj}) + return self diff --git a/storage/append_from.txt b/storage/append_from.txt deleted file mode 100644 index 75b9d029c..000000000 --- a/storage/append_from.txt +++ /dev/null @@ -1,3 +0,0 @@ -ROUTES += [ - Get('/some/appended/url', 'ControllerTest@show') -] \ No newline at end of file diff --git a/storage/file.txt b/storage/file.txt deleted file mode 100644 index 56f44d360..000000000 --- a/storage/file.txt +++ /dev/null @@ -1 +0,0 @@ -HI \ No newline at end of file diff --git a/storage/logs/.gitignore b/storage/logs/.gitignore deleted file mode 100644 index c96a04f00..000000000 --- a/storage/logs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/storage/some_file.txt b/storage/some_file.txt deleted file mode 100644 index 56f44d360..000000000 --- a/storage/some_file.txt +++ /dev/null @@ -1 +0,0 @@ -HI \ No newline at end of file diff --git a/storage/static/__init__.py b/storage/static/__init__.py deleted file mode 100644 index 9999d58e4..000000000 --- a/storage/static/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Static file storage directory. - -This directory is responsible for storing static assets such as -CSS, JS, public assets etc. -""" diff --git a/storage/static/profile.jpg b/storage/static/profile.jpg deleted file mode 100644 index 359954839c4b7e3cb5f6e840fbf6a7aa63cba8ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 237344 zcmbTdcT^MI_dXf~2_T>%QNVx-2tu$>f(ad!7J5m5P((mVP$YyJEFfZuAv95lq1Pk? zq(nf#7ZehDCsY*)5FsK;5y5tOKi_iKx_{l@@0nRMd!2RW%z0+^oU_l~d;U)Rodp~~ zm|K|x1Oxy8%iRs|_aoqiSxj&M0AOniPzL}2A^;%)Il$gsX}1c%NZ{!I%H{&60D}K1 z-va+Cl6FaE9oU=BeyB5vS +*mF=o@SwoocEGXSUhft7 z5B_u6?qRo^LVJaQBBEmZb}KLs0QLw73hogS+`ISR+X&qHmxj>6y;3JMO@$9RUI8j% zrL_{$3PhAHRJVbghQ2Cm`-a~Y6+3)HMpo{mis~sfbsebgc^F*p;w3Y43rj16vkMaC z>gJC2^S>Gp7!-`bMMOqL$Hd|j? yB{(`Q-P f;=DeqoXKX=(Y}+WN-k_pKk>JOA?9eYXEaYxnqH z%>F;T4({^WBP1j!1pJqmz@F%Ti60c&dqPuK%G42f1$#(QD?vp1LRvv}o2Zhu(^rsh z_>kCPWu1>Fzx_+?Kg|CB5xe_;#q58H{lC1X0T%{sX~1dj$6i3JUHO-n$E75#fI! zA|~=5i2XOj{{zW?vHySZcejyUrh-C3Lcrbqz&_D^2mX)o_ua0FI{cjihzknrDwE(r zfHC0b?E_XaeDbH$3GyLg1FGOa(1=F@DP-H+Vnf>YlxrGcl<1c@in^%*5UynXZ0y7J zT&M_ABr#z!>=IXZ0lsq}Vg6RP0+v%`;B P4@W@ zxR%^5tu299rFQgh8Isjm{`55W{^mtyUb-nwtGp_ME;w1Hq&cokaltf?l>x%^M|}Vx zeNV3SZ=M$LTXm-G5zy(7g pn8#0olo$d^|_DS1b zB+S0V#s#2hqWsNEOip%POa6h3&V6l^r9mJ$bU$XuY)^)IW_kBmjW*{w^Okz7wUfGZ z7wD7?g^+cerS5QVqw &`cBfdzT^p- !mvK4 z^yB-X6`zkPLG#pjFRJp(AVqgAyJH;W=dtcb5`$EkmtVcI{9(lGfA{W2=fJU&Xxh`6 zfv1ZK1!^py(Cy5Q%Ww@VF8JqfNL52`V(H#+*IA#`n5E6ihpw+57$PuiNW=%tYx6%h zw<`3r_fX}3?{G4I6@(X68h2G5b!d)~7=SKcaJ6`*<2=d)LlhNq^fxM1=J;{ds;S0e z!>= 0H62|jFMeR0ZPtJB_b`#bFr;+%!eACPiNk}A|J!S@d0{SJ88 zx4b7@dli*>D5VO04@<(58|5h24@_8PX;7|a;y~XHhj{61&$!W;mF~vnYhxvbA}g2b zg|AO_#~+B>S%VZXGo>p(1IAcyHD#1qB2c(9LmxmPmqb5B9bTnKvY5xSA6&dE3CXR0 z4g(M+-;Y%tN^S)Ip^REz@^l b#t2zsM=!llD3)byVu_dN9HOw^~HR}=* zcQp QMYLq1l!z5VoQsCv*nAJ(tUdG*+N%M8b%va|{B>%}z*+vu^% z0kmdy {Cdv%-aoi*4#Z@&RhI*W9lo@;94)Z+2Tf#ru zG<$vj)0$A=Wm69=@x_`m5BI#jd}wsDyu~hR n!ncv)JSF zVL|zUkd!9cfjvkFH%r&&+&dQ!P=Y2~Jvd>Y899LF)TiWclq)`#hcjdxz~bxPhVC;M zy=Ojlz>*+Efiwa`q;+YKbI)f#e)dM;@(GJ$$uL2Ra}&;VDZ?tNQOQm69DY=yb+Op5 z-=xE&9Y fM=Pc!g`+T{_XS zY)0e?`2;YpyR~`Gt7~C~&DPd}mk#uc(1+fL${zdZIh<{EfGul~tLdura2`MQq@GJ) zf`xoG<@xDIDd@rpH)1A2hIbTh!+ZP8Yq*6X2&2hRFI*tMT^(DuRwfE z{GJe1#dHmi3$Nq9{kfg_PspB}=78Gk1wN)LULabSF~fiJbZD^0^chh$H2;Jn(Wx~{ zc9VWrdzhIF^;2zhQB1R5pn^%6Vfh^bKm^?J{Ha(GY=Xn@ Mzn}7V@MrE&RDuV#n@-FP$s#P7 zh?OxsNw?np=5L?q{d3=PNh;j3i*agkuJPeQ|A;I1x!~ZqM)72s+K4h!H7`jJI`BK6 z+NF~hq69#N yo?%veu`oTeWsz=_zuO{KrU3z~01&9Tw(_{@Xhskxh zCzTPU?!lO%UiG?+xG^;q#b)%69{JQl&)}A2rPF~rY(tE(_WNdc-!D-#l|-uqr1CQZ z7nO^A jb^>c)>v{5*yzzF*n?mN1drH qLMSP!CF&Q{;{F{T_g1P+- ze1EJaB^or+cgsctO}_DDXIz&0ULbP--f``n?aMQG-v(Q|SxQwEw=dE$I$D!jol L98@CI0S3exLQVj^ zF>fs64zcerv XY1(q`>QjYd*NZYE?|nI=i>eL>vzLqui?leFB{v~>y@$K+@t@L zVK^@@+hsl}@%Kbd4qf`vLt?iCu|B(n3KDvmoq#*W=y#8U*|LVhkbWwfeeDYFvaQY9 zjDj}dOU1cXzpDDRN@v^n2G7}mmVGvLY|OT2=!p>A @Op#Zg*pxhJwzX5K}j(`b`7 z_NdO?mJPBUe_!W`?Qq6kHuhG%CY#GA6dhkhhod5zBdfC8Aqttgfjz7c;cu@9lQ|>_ zT>W* A3{iAO7*=0zuVr_k}mhO)RkcL@+FeCcbr zAw<64&K}PUU&!JW(7<_bYD$FDMzET>YJ3^kRvAqy45(7^5KFqFKK1*7Z`rdn%CJoQ z7BpY6b!+K|OK3b8e;ay!+38W%l~wx#d$dvQfriO8)${(}3;KJU($zHLY&{b^Uhq+e z#lbsc54mk~jSCJ@7mI9A6ihgbMqoyrWCO_`^RBqnQ6%?zrIffhE}@31`*4??Wv^Ki zN6);Y?#t-A)Sg16QmU%S7kI|Wmu5zK#FCS3K;%6@G>yw)beag$s#m=BhiT7qm)vW^ zV?ZUU@({hyGO%lbb-rvkw_vW;A6Oc4!RsBK(ElXzPYZvnn0cZm17K^$M&~@qNY_$J z(j{4dX7bD-2M>f{j@T_SuJZ^Nw114LVs)G+f>VwOMbp~^c(C$!=6Iy4pDtOudp*Oc z5MXF)*)_m`ORpI&PdA<{_gUX>2=UhU2FgPB77Da-p;dB$!p+@G1y-6B(falT-AYW5 zjm~z?&FXV=>x5Sxap1H=j>vS_y&&eQJM1pV02GA~>FKK=+XkiK;sYx(N*UY85t;tI zhAtq3K iPVaf8*fSl6U4Lu}QJDp-oc zO|F3?vp}*CyAJ*fK(HeExXRIYuDxT$IXy}NkDrwPg3o)yoXvXCRtaZF{FbPK@iLs~ z3$+~D#iNZ~ORn5|iMemrE8s`f=a=VwNNUrM26V
_-wB^d@-)>Ys>~CB<@A4&%A3cJ9hn>$J1wd(UkFIk9{Ypr%Uld z&b*e(R{XU3M%5Nf@pda{(Mku`pWOdQMU^tv{*1u$%kmMcmv04$m1MW+shVU&fFe&; zVnfZ)*0d?u^PmBv2-zWx?6TOq)#<$VrH8OyA<|8h(B$Mp& 7)aWx}<7VX=G^$~3{7s9IRvaq9U&E7o3jXL5%X z_I;gis%r@%8m7AM2&zO7BzKT-J7nrEs 3uhT(dZH!RR* Pvzdub`H EuSCtYmqVmJEy&&x`|gMM#0!VTy;O=a z&hhC}v0z@iykNcG*|Tr7f`(<`Z$XC2jw WerW|TBfjrYbyOWoSw-7^7x)JSpmm!@9Q>2O~dt4 z`0j$r3;3w2D6!79KuC3~k2uMG!MTuIz;`U~CVl7}E0LM2)4YaHev9QXt?G%nMVqBW zfIgla%hJ+w5OfRmBV@Yb_OI^@YKcnpn;C=m=p92a74b7SdiSr*6-!VAhf`0myYW%q zse8VS07>dk9yid!isUW$=mVZ0AFVKmehSsluw00$`VR)2gsZ9h$Fv?hKYnrV988=M zANO;dTuiDEnT m&VF-Hpo}G#wP o}Co{e`C`P+dyf?iz07_Titcc4 yHk{5Qx6l z$VS+jFWOLG%eLnIVUp^N7^g|ur48yo9)LOg9KD`toh$N5;==qbwF2pq&5ETLcKc=M z!(c*@8XWX#SoYZ|p28ny&MAZM3^-7UG6p}is~jc30N>TY!Xv7{GR);ue)1Sre=OK} zRPtUxJ#X*rk{f3Y{$w69w@|Em=@Tf;dTbbkgbD{_>Jaa3(o2m)gNYB|P>g@0Eb;Yd zxqjRj73c%x6e7Xjc_k4l#AJw%*6XoaKsTW?AuQZpX2m0w7$4G8u^I67j3Nc+{8=ft zZghiA00Lz}i*Wtt9@eRQ>8FZEUSbnR$vF;qr1PuHF8?_FK;z+JB;YT=)Xra3X2lMv z#EgWWFL_Kb`|&D~JZ5u1v(JxZUxgS<_;VRE7AijR@a}$whw}$U;3ez;=9OrtYDuJu z?-1--AXH@)!N?$cO&W~NJePXfcRmV;4p>D5EI(~iw4E@B(1|R!mdtJ|gG6+gu@ENx zY86cn45_&E;AqJw9llS7_Q0V@xWg9#A%$7usFR(1&&Z-iSL46KTjsBnzJR%FrN8Nv z^TvpN_r$=d)Qo;u)jT=G*8anDXi 0914L67Hb2D#OpyexT|v0A1 ta1Ca0!EB6JIn($41(<@0Wr<-TZL7Zg`g5YxPz< z0;<)m?e*<>Hv4mhM`y7wysO+`h*K2H!g{GIIr#TZ(0bnj$;k^siv_VT>Jz(%t?pnP zp>TDS?5?~*F4C9)yDH~TPK#t52BED?V6<@0XquDfUHo{CQ=Z>TO*7VW;@J5-rbF!d z;3Anp$?CI4wuASm|9Ic2&6{aJN}phsVYHtdQ>X+R>rnq1)%@Dm1cKn7g?cpBYzA z*`8C+-fl5O0%SoCWuoXW8Vva_iBnN<%*Bg^B=ieqG|MV0ESAc#S%3ffex}E#Sf^r) ze_ERDyEa;(10L)!?2jF8D*WQt>2^kwY&%Db&7La?z7 BqdCOSuZBxVSEOETaJhRWE+Aj9|d33*4sCR71Qyb!@ zA}#yeZRVHZLLB0v^0j$C25zfz&jC9TVdkPw*`CIC2=D#8RqtXockpl;YuQFn!6=jU zuJMeef$yv92-9^*Ueym*HGQi7qYepZ_48BJ@^0(D=(=kkk#2!i$MW#Gi_1_aa~R*V z DxMyF -$-KV4cSiz;I(+ e1n#cll-!lfiFw#;H?&%*J^CIMmGR8?E( zgrE@VZXM&A*EeSLDe$=0l>&o@C8@;qaf&O*^U{gNal4GPz9r77Vf9|w)LXN#&hM3c zt6|TYmvAA!iO9-lHeVa}x|BhPd3Vqo?^NH4-2?mIf-GRODZV+`U#@&5b`RE-3V{`3 ze@WoKXO~KW;#p?KR)YtlUU+XV;iI0?KvCn1S-o-3hw>cF?>>4QSa#ruQb}Y~6tIEu z*y|iX6x6gg7bYlE^^8*HII|k62|-U^@3>)ly|wIO2odXX?rj2LsmQcNc453d>Nw#8 zDDs4XZ$ J86Qa*&qSZ%Kh`b-1T_AvCXSni zSwIKgD=@#pizA{8n{|1rH{XS%j4Dr4c5R;{Zy8uTK~re%AGzKexG(2kF2H)0x|AbV z2#cY|aEImj_&>Tr-z&^7v~N{qJn{O1e|R6(b8y!rcaR40wuhuI`S@0`5B}QWEa^P9 zcRHB@S79jMN|B{How=QfJ<$(ay5#ioKC$?KpAzR;_wSHP7p>=se*x<6cdi-vK6s*u zI02qTeiPl3Q!6_FZFYKDv}hh 7dLi0|cf(5&t z|HLoWgR#DPBZ8a2_41xbotf`^w}z0$j7;{^V-L&SNWty7%!9k%8uH3@Qwe2)NzC^W zWjVhvJ OLiBCWv~ZoEF4EvUru=PX zh;1vZ4;iy }hzk>;3DS(mied4XsC8jL(D+=Zqhn5Fr=+tWK9zaC- zl*T$*-&Ix>bo6*XmK+`Q?2tV7_|E9wuoxM|x_3ZKe3>d}NVZnD!XDGqz{na+a)@25 zy`CWs#7q(W!yyDZl%QgA`!1urt-R>rdanAiiam&DMb9~0BV^z+T2zGwg=-F%h*`3? zq0MsExWmU~WXmXAg1tsYpK)M!%n~}zVrb7GKC*MJf;ICKsm#LoV@jMBW+vSMa4_=Y z8IwPmcU7i8WH_Jx@DFsD=5{~rMGcpfyHVMJIN+o_qB6I~BxsM&5tlqSp4xf3t9|5qHNa*q|3YrW@&P8`u{^Guo2Ydvsh! zwb9|6#FclR#ACPBi8~J*DFSXd@55Z ;H}`uzW~LT zjCy|nY@_dv$}s8S?=L%;Yo?+wF~i^Vvf6u)#L e1!=vaO!12Dp+zX@zJa3vx*7i7x|KLV?NtFqBC*&j+m5$wNR1_ zt%l-1dKQ;7%Y9#4@=8^)&hh%=>+KEK*`V~HFMcy)dD_?kbddZl{J4{6d0$ga4ZFU? z-SawCWMI$p7hLFD_jgC6+}4{o%#Yrt6?Ld%YF}>QQH;iUEcr)o$mb|ia;oZ{ItRbB zDe9#ON9RW|eK*ZQUIvFxVAB%p)&a86O9-p&;F8vD`ar~&i_iQVJokU3qCQn>clbTB z{G?VgvB6M6OdpCK{e=3t`}(~F-!~rk{LeY+(}miOXlGlgP5pyWhQS73?O~N(p>ATS zUN5?;?Bud0<@b4>>Oei^ZB!g-4%hpJM1PGJ4k??^J*k`=djn>`aT&j-z|1iGA}R|? zgD&wJ6*h;!@;yk2t}|>HrsU)Y136yt>(Iuzw19x)8Dy%DlgCkW{PV}UcrbGQ_G{*Y zL*M?y(7$!N9EgjqB6oaTiIX1;cw>Z_2_A*oY?Z0}_NA;vAfl2EDjg+$FPzs*y8* z$>;;h(;w&LW6uDgzG~+}&i29;OWlFqn|11L-7bBbw_(($o(d2-*+()e3{jD)^0!Q7 zik+wv^itjH5Cvwg@`~lv-BsYcbM?UIp2cg(=;yK v{})}V_6_} 51LA-1w*r>Rl_ijyhR;?mFQ>mos#Y1wS?m{JI&76Vfn7BRbdY( zapRN_y+=3m;afaLRNQHCcyEh^ ML358!izV_9>eer80W_i9Iwln%B zfw-Lh&NDIo!&`m_&eip-5_StWoTt;f4PTBshVGB@%jJy^x4@hn4VFe5!CAKpsaBMQ zS7UeE?CclZo^5&DMgo=oHAY_*9c@w~*$j+@wmbT7D#=tz*}u2ETp5D?j>^55i*t!e z>k-d|jFl<4gpEGaL^>o;uJhBJKDRBUe5LCfIk`lZ#m1;>r|YeIsQ6rbA!&iY=d(oS zBdEFxIktwT*V{?J8zn{omL29hJ9p5jYP0dWBW?Y_D^7daJ;Zg3^CqLo=6b|5=W+mX zonZR*VCw+>@&eyE0{O^fIk+d)TrXE$w+$BB(nxiYijmbf3vjyhNY=03_}r4mR8;oK z_JTcO_OSAe6~js|%L_3ptHUji-=YO(XMat=H+IIeA9t8Pv0Ik*oPW5HB7Xn)lNoGk z$jHWkgjBxtXYGxRO1j0MxpWN)Sb{gtDW=hKn#UJmpZjfF>|Yezw^B+U{-q Vr&?M(tg}7@# z9aVDs9ZMH`J-Ug=c4p#Ji~R08%RF{fvRSvRrnr2Pz`IZ>>6u-QnBgkfatHiarcve> zXUmVzPdbv+5kaaXy^&T}KV&qi0^5UCYZZfUlrX&8#g%SA_ia)?z?P67j7!3Lr<7IG z65i8~A!=gyt4PYuc$zbHTuM}LOzmikTiO7=00MS>@GvLsgWXBB54$$&Y04rq6uj (7I3f4}0iqwq&TS2MKr_|I|8IN`52FRwQltrgUdGn#~7yrUMBotp;R_OPbv09C=^ z+GmLQmos7;YNvsoku=tXf5K%Y1BkuzB=Qj4a^qv$^i&1ov0qZ 7GA7GDQd;tmgv=vvqPz7I2^$J#yLT5POu;rppYUJOMAv8HdDz6oAMXnW$vL0 zV?#SsZ;3fBTelfFg#itNJae0mAv^=qI *;d+jq6TVh zni5?+-6hO;qEvF?VlHK{>{f>_3AR|6`t=rV8lnLBg{g>NpWu9rR_p*O53BpHJAruQ zYH=Y7Ve0z~O68Gms^&O( z!&;lOJ82_S|!&*R%`o`bU_BFmM1okaVC5%W0IuNwGfdG~?+H^(NW zocvoiJ52iOk~S6@jl3nKti8nh#>FOS^WplwRR`Jat>T-+6f_lbuygK()9a+-S7b;S zHg}S0)}5G&cZHrDBqGiJIZ48!zLi0OtgENlu@uQan^!|Esx~X-&swfhBB7$u zmvqzKoo2!MG*=ykt@^@hFt|X#CavlYBof9L&UvKAPj&{RwxT3hMgJ)0t*^M*N2;Qr zwu_ > zUReXZ*w)3y5D(W@_VGWvX>j+wfuWG?+fV%bZ!b>kJ$KJE0iZq~QDeh4EA`ae!(s-Q zUe>j(uqrc0o4la@wf9SI06oIk-6cCMyldXY@X47*2JFj31YD*1slsA)dGv*Ozbc-i z6^^tG>a~cm3&%Piz3(Gp;HqEX7pe0dnmeDyhSL_!=LN}y%Bs{Q!p%FHpqtIbFMGam ziBVKjz5`W;jmEDTCSN_BaQMh7?)|QNnBsEkyj(+(i0G2Ht|IzqL4nJ8dG%4-`+d%* z$DxWg`8>wWk{E=|q#di~ ZXpY$w0>xD2lf6Q#dZV74#o$^fjqi#lM>IN>=*;|RB-ra5VOp>ZP~`$jit zxNcz+L;djrGp~%3`5B%*;gB;S{gV3;yC#3QtzX|+n4!e2GYXk2xa_BA&T6KM^j)!y zd&*CuZAA+Of(CjbGbmeCr6oxo*>M*OJ*qBp80;`tS-vOXT|iY^*gi=$&YMp+M01FT z7EJqsE8@TEM^$=HaY@E#4k64!mHeY3;OMRWC`w&p3z(qUkJ$(ZW8K1fVVLIht z#?M zbQ{hXe!Hn7#S7UJhboR1Xso W=V&sQYJ}i&ataddzm*aO}Ns`;YG5 z?Dhj&>>K2M>S1kFQVdDy7rSWbHkL-Cl|MRZDZ?$x(ssd}RO|^sLRZ5x1Ju1qc8h -qb%-VuKMV3&V(}MFCZ79?k<+m=jk`G@ce45*jnG8 zJ++HjYRCOZ@zGS(a4S#0C3h$PJk#-Gh-H^?=d?~jT*|YBdCKNSrT3R`BhN1b2AqzO zdrnsK8 Wwg@TLJN U{J#9?PUJ}pEyKTdOCF~c?4Ew?0O6l~@Vo#7yu-G=(;@36gb z2TDn@mO)My2BQhWhKAH-fHZTsFfGwXSXkZvC-aC** _P&{a#AVtEZV1HE=)2Wh3mHtoCl4iiWh&6YCD#Sqi{R9?!9^r18tU*j3al!lTn z>X`M}ROelaZRLI!qH75> z)ZZ!#)9QvBGhnKHxf=}zq_M~8@27K)NSoKd2WHJCFUwV8oAkt9#z_lq}8DKsP7w ztKHJAfK2(d-2{|${|tVIBI}(*ENA=zA<2F?%O$S_tt34oek&wsYO3b5({$%tu_z1c zd>!mu&7pOLvb1{yETr#i66Y$^6Yt`kwquTy?myFFem)G5Y27N%(fl#?7of48NKQ^A z)m65L2FuzauU-hZ;G#+K#4*&xyY>i >iKPqqoxkW#Q(!5}fs&y4 zg~Ee6VI&bj!4^-_yQp79-#R=A69NNJ42%RQlQZlHZw~v3x`&nARp2-C G9`|sW>;nn+uZLR0m`d4wZ_iXi@PL?bf{VxCiTrMDA$KJ(0b#; zLTKsJ25IgY9`}GUmGI%9Vq6AGR9PHl*oqX~Q(x%;7I1SFvsY=5f3k6E;mgDqgIVO> zn_yBY^UIT6xA@8N4DVvOAeQS)F5FaR$?411T& B2a>fY{m@{H`H>6w!c{EmOOT3xFC~uFFh?I z7xL;rqol>(vCU76-*I=;2Q4)T_yQm(!mrf$;N_ktN2`A}&pEs81SZ%|v<5j{g#|_k9g5c< zeH qX?Xh+s7soR8w(-mbhF%F^S4!r@=7z>5t$%TLLdBJSGwf%Fjin-$V& z&t-ot|8iC*!HqKCs%wl)6&2KPlk5?>!BJ;`eD{Oz;>)H%dAM@P{Ke+&kJB^%VA9H< z!&Mi_m1oVrG*miq9mUze-Bb;)JZ{x@Qt&g1fjJ z626ZJ-hi${zxMMk1w>SL-jN$l?w?Tzc}}>(f`N)aEMvKE>J9 2!ifG;XVozny8PPoJ$&R*3YplEASQ6iyyB=D&Y*Q)xRVtcK^3 Q7AC`f9ka!OJSKM8sAMMa+!%O$WH8fVGZ`v!S=e_C)yUd11MfxR4ZFMpDKzTo7_ z;$ZAX#aZSaD|tiye#g;%M8@unrN{EJ;Z-SfGcIE-_6IG8;V{wr&cT#^+{y8}>+2@I zS*b2>a IFxf@@(pF&{PBz2c3iA!Xz+iuu@R7dd8 zpbI_KaG7{L_Xj;FcAC?4BC$G8Z{lUTk0fTfJ8E;uMcQZHr6Nk)wc1=#*Zk1cTE&cY zq-&VrZ`zRz3ZYCnnJ9SK)>Ppp8_}BEu*321*G^!U7`e`7xa0IUfaI;9jz~CJ%3Y7N z*M9~ YYAlTXjt<{q=G2nm(FSIvJGPbgG%|v?kPh{77)#vM) ztM6OHA~pKmN8qx9jRUv;Y^H=&=xweLmcA5-S*Z$wIXe7|abv^^5(-llBtKU={>SSM zx;ORu0RGb%KNe!$?TC!T{a?GkFuauHiT5^Mpmy<~8CW$F>^P^&9)jl@L{!8MsPetO z!8|m-WYr1`O8#!79hVfC=P})GJ{?d=GM)bQJH+4bxC`4muf!4=hO;+KBo2f8+N664 zKdpok$R-WO_cc)>vnfp-=f0U7NX4i1sd;9Q&bvH#u|)O+Q)YL5ItZHlGAtL ?S(G zQrApivwuKK-g0JA)He9i5_ug<8q^QtJCAPcQwcCslf8_f+Xs+xkSe43P!OwzQ z|A>9hkvNwq4_Uj`Ulcg`v8dp~#Qr9}msg!1fV}wGHP~^<%eHE!inxrw`eS^Gk-tQE zWUk&Da#t8qqI5r8S`dM9N~{Zx9DVqSj}^$joE=|W*31^Fp;E?Z7~u2ONG@cq PZRqhZ?w-f+v==j8D_|wj*`^!r}EsPO)GQUDb2{o|!b^}Li3IV93lJhhB z(URRPc#%A@M7~Fnd!U%!XbfzT-6}sXb|ph$Gq0{1vG+xoz0U8+B^M;yug_}H$FCBT z=~3t@YVAk9SFNh^yp~fG@EIc8zfhhA-(4o3M`R{ncx&ML%w_qJP~tGP2Dt{{_nCK* z*J#d*dPNY~v$A|-?g_$VOv46k 21E?;gz1>y>b2(kWjbP@FX( q0tl&SXC`T8n{P^;Arn-#-8km(4Hja?yJJSJ0-EBE$$X<#P#qO9O? zELzyW