From 7f30c2820c6b98d0c87a3a44235dbd27d02cd495 Mon Sep 17 00:00:00 2001 From: Joseph Mancuso Date: Sun, 10 Feb 2019 08:34:47 -0500 Subject: [PATCH] Next Minor (#553) * added new config helper function * flake8 * added docstrings * flake8 * removed need for config prefix * Add in container (#520) * added contains method to container * formatted and added tests * bumped version * Add ability to login using multiple columns (#521) * add list to auth class * adds ability to set a password column * formatted * fixed space in exception * Adds route redirection (#545) * adds a new redirect route class * flake8 * fixes issue where a json null could throw an exception (#544) * added ability to set status code in controller (#540) * added ability to set status code in controller * flake8 * added better exception when passing incorrect parameter type (#539) * added better exception when passing incorrect parameter type * fixed view test * fix delete method when body length is 0 (#529) * fix docstring for secure headers middleware (#548) * bumped version * Adds ability to use list for route params (#552) * Adds ability to use list for route params * added new assertion * Add new data type to be done serializing after response into json. * Fixing statement else if to elif * Inicial code to test response. * Assigment the serialized data to view variable to return just at end method. * Add test to verify the return of Response. * Wrapped serialized content by json() method from Response. * Filling more code to verify why Response is come empty. * minor tweaks for tests * flake8 * added ability to specify routes in the initializer (#559) * flake8 * Add migration message (#551) * added ability to show message when migrations are unmigrated * flake8 fixes * fixed command * flake8 * Adding statement to verify whether queue's driver is default. * Adding tests do verify default queue driver. Closes #564 * reworked queue connection drivers (#563) * fixed login method (#562) * bumped version * reworked queue connection drivers * modified queues * Masonite can now retry failed jobs 3 times and call a failed callback * added queue route * added failed job handling and job database table * fixed queue command docstrings * job failed methods now need to accept payload and error * commit * can now specify the channel you want to listen on * flake8 * flake8 * removed queue failed command * fixed command option descroption * fixed failed and fair * modified test * added base queue driver * only queueable classes will get requeud on fail * flake8 * upgrades async driver * updated async * cleaned up class inheritance * flake8 * fixed contract * updated queue contract * added better test for default * bumped version number --- MANIFEST.in | 1 + app/http/controllers/TestController.py | 10 ++ app/jobs/TestJob.py | 23 ++++ ...9_02_07_015506_create_failed_jobs_table.py | 22 ++++ masonite/app.py | 2 +- masonite/commands/QueueTableCommand.py | 18 +++ masonite/commands/QueueWorkCommand.py | 58 +++------ masonite/commands/ServeCommand.py | 4 + masonite/commands/__init__.py | 1 + masonite/contracts/QueueContract.py | 22 +++- masonite/drivers/BaseQueueDriver.py | 50 ++++++++ masonite/drivers/QueueAmqpDriver.py | 119 ++++++++++++------ masonite/drivers/QueueAsyncDriver.py | 10 +- masonite/drivers/__init__.py | 1 + masonite/helpers/__init__.py | 3 +- masonite/helpers/filesystem.py | 9 ++ masonite/helpers/migrations.py | 33 +++++ masonite/helpers/misc.py | 12 ++ masonite/helpers/time.py | 2 +- masonite/info.py | 2 +- masonite/managers/Manager.py | 3 +- masonite/providers/AppProvider.py | 3 +- masonite/request.py | 6 +- masonite/response.py | 5 + masonite/routes.py | 26 ++-- .../migrations/create_failed_jobs_table.py | 22 ++++ routes/web.py | 1 + tests/helpers/test_config.py | 5 +- tests/queues/test_drivers.py | 7 ++ tests/test_requests.py | 16 +++ tests/test_response.py | 36 +++++- tests/test_routes.py | 17 +++ 32 files changed, 448 insertions(+), 101 deletions(-) create mode 100644 app/jobs/TestJob.py create mode 100644 databases/migrations/2019_02_07_015506_create_failed_jobs_table.py create mode 100644 masonite/commands/QueueTableCommand.py create mode 100644 masonite/drivers/BaseQueueDriver.py create mode 100644 masonite/helpers/migrations.py create mode 100644 masonite/snippets/migrations/create_failed_jobs_table.py diff --git a/MANIFEST.in b/MANIFEST.in index 4987c254e..215c8789d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ include masonite/snippets/exceptions/css/* include masonite/snippets/exceptions/* include masonite/snippets/* +include masonite/snippets/migrations/* include masonite/snippets/scaffold/* include masonite/snippets/auth/controllers/* include masonite/snippets/auth/templates/auth/* \ No newline at end of file diff --git a/app/http/controllers/TestController.py b/app/http/controllers/TestController.py index 229e6f609..d1c4464d7 100644 --- a/app/http/controllers/TestController.py +++ b/app/http/controllers/TestController.py @@ -2,6 +2,8 @@ from masonite.exceptions import DebugException from masonite.request import Request +from masonite import Queue +from app.jobs.TestJob import TestJob class TestController: @@ -32,6 +34,14 @@ def post_test(self): def json(self): return 'success' + def bad(self): + return 5/0 + def session(self, request: Request): request.session.set('test', 'value') return 'session set' + + def queue(self, queue: Queue): + # queue.driver('amqp').push(self.bad) + queue.driver('amqp').push(TestJob, channel='default') + return 'queued' diff --git a/app/jobs/TestJob.py b/app/jobs/TestJob.py new file mode 100644 index 000000000..bd4c31a14 --- /dev/null +++ b/app/jobs/TestJob.py @@ -0,0 +1,23 @@ +""" A TestJob Queue Job """ + +from masonite.queues import Queueable + + +class TestJob(Queueable): + """A TestJob Job + """ + + def __init__(self): + """A TestJob Constructor + """ + + pass + + def handle(self): + """Logic to handle the job + """ + + return 2/0 + + def failed(self, payload, error): + print('running a failed job hook') \ No newline at end of file diff --git a/databases/migrations/2019_02_07_015506_create_failed_jobs_table.py b/databases/migrations/2019_02_07_015506_create_failed_jobs_table.py new file mode 100644 index 000000000..72ef12560 --- /dev/null +++ b/databases/migrations/2019_02_07_015506_create_failed_jobs_table.py @@ -0,0 +1,22 @@ +from orator.migrations import Migration + + +class CreateFailedJobsTable(Migration): + + def up(self): + """ + Run the migrations. + """ + with self.schema.create('failed_jobs') as table: + table.increments('id') + table.string('driver') + table.string('channel') + table.binary('payload') + table.timestamp('failed_at') + table.timestamps() + + def down(self): + """ + Revert the migrations. + """ + self.schema.drop('failed_jobs') diff --git a/masonite/app.py b/masonite/app.py index 214eef3fd..62ec7fac5 100644 --- a/masonite/app.py +++ b/masonite/app.py @@ -239,7 +239,7 @@ def _find_parameter(self, parameter): object -- Returns the object found in the container """ parameter = str(parameter) - if parameter is not 'self' and parameter in self.providers: + if parameter != 'self' and parameter in self.providers: obj = self.providers[parameter] self.fire_hook('resolve', parameter, obj) return obj diff --git a/masonite/commands/QueueTableCommand.py b/masonite/commands/QueueTableCommand.py new file mode 100644 index 000000000..f16ddf9eb --- /dev/null +++ b/masonite/commands/QueueTableCommand.py @@ -0,0 +1,18 @@ +""" A QueueTableCommand Command """ + + +from cleo import Command + +from masonite.helpers.filesystem import copy_migration + + +class QueueTableCommand(Command): + """ + Create migration files for the queue feature + + queue:table + """ + + def handle(self): + copy_migration('masonite/snippets/migrations/create_failed_jobs_table.py') + self.info('Migration created successfully') diff --git a/masonite/commands/QueueWorkCommand.py b/masonite/commands/QueueWorkCommand.py index 593ebaed3..109e845d6 100644 --- a/masonite/commands/QueueWorkCommand.py +++ b/masonite/commands/QueueWorkCommand.py @@ -5,59 +5,31 @@ from cleo import Command -from config import queue +from masonite import Queue from masonite.exceptions import DriverLibraryNotFound -def callback(ch, method, properties, body): - from wsgi import container - job = pickle.loads(body) - obj = job['obj'] - args = job['args'] - callback = job['callback'] - if inspect.isclass(obj): - obj = container.resolve(obj) - - try: - getattr(obj, callback)(*args) - except AttributeError: - obj(*args) - - ch.basic_ack(delivery_tag=method.delivery_tag) - - class QueueWorkCommand(Command): """ Start the queue worker queue:work {--c|channel=default : The channel to listen on the queue} + {--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} + {--failed : Run only the failed jobs} """ def handle(self): - try: - import pika - except ImportError: - raise DriverLibraryNotFound( - "Could not find the 'pika' library. Run pip install pika to fix this.") - - connection = pika.BlockingConnection(pika.URLParameters('amqp://{}:{}@{}{}/{}'.format( - queue.DRIVERS['amqp']['username'], - queue.DRIVERS['amqp']['password'], - queue.DRIVERS['amqp']['host'], - ':' + str(queue.DRIVERS['amqp']['port']) if 'port' in queue.DRIVERS['amqp'] and queue.DRIVERS['amqp']['port'] else '', - queue.DRIVERS['amqp']['vhost'] if 'vhost' in queue.DRIVERS['amqp'] and queue.DRIVERS['amqp']['vhost'] else '%2F' - ))) - channel = connection.channel() - - channel.queue_declare(queue=self.option('channel'), durable=True) - - channel.basic_consume(callback, - queue=self.option('channel')) - if self.option('fair'): - channel.basic_qos(prefetch_count=1) - - self.info(' [*] Waiting to process jobs on the "{}" channel. To exit press CTRL+C'.format( - self.option('channel'))) - channel.start_consuming() + from wsgi import container + + if self.option('driver') == 'default': + queue = container.make(Queue) + else: + queue = container.make(Queue).driver(self.option('driver')) + + if self.option('failed'): + queue.run_failed_jobs() + return + + queue.connect().consume(self.option('channel'), fair=self.option('fair')) diff --git a/masonite/commands/ServeCommand.py b/masonite/commands/ServeCommand.py index 36f8e9229..ff03a46f5 100644 --- a/masonite/commands/ServeCommand.py +++ b/masonite/commands/ServeCommand.py @@ -5,6 +5,7 @@ from hupper.logger import DefaultLogger, LogLevel from hupper.reloader import Reloader, find_default_monitor_factory from cleo import Command +from masonite.helpers import has_unmigrated_migrations class ServeCommand(Command): @@ -19,6 +20,9 @@ class ServeCommand(Command): """ def handle(self): + if has_unmigrated_migrations(): + self.comment("\nYou have unmigrated migrations. Run 'craft migrate' to migrate them\n") + if self.option('reload'): logger = DefaultLogger(LogLevel.INFO) diff --git a/masonite/commands/__init__.py b/masonite/commands/__init__.py index b33e5ad4a..f250145f1 100644 --- a/masonite/commands/__init__.py +++ b/masonite/commands/__init__.py @@ -18,6 +18,7 @@ from .ModelDocstringCommand import ModelDocstringCommand from .ProviderCommand import ProviderCommand from .QueueWorkCommand import QueueWorkCommand +from .QueueTableCommand import QueueTableCommand from .ServeCommand import ServeCommand from .ViewCommand import ViewCommand from .ValidatorCommand import ValidatorCommand diff --git a/masonite/contracts/QueueContract.py b/masonite/contracts/QueueContract.py index dc409022b..b5015c74d 100644 --- a/masonite/contracts/QueueContract.py +++ b/masonite/contracts/QueueContract.py @@ -4,5 +4,25 @@ class QueueContract(ABC): @abstractmethod - def push(self): + 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/masonite/drivers/BaseQueueDriver.py b/masonite/drivers/BaseQueueDriver.py new file mode 100644 index 000000000..dd017035a --- /dev/null +++ b/masonite/drivers/BaseQueueDriver.py @@ -0,0 +1,50 @@ +"""Base queue driver.""" + +import pickle + +import pendulum + +from config import queue +from masonite.drivers import BaseDriver +from masonite.helpers import HasColoredCommands + +if 'amqp' in queue.DRIVERS: + listening_channel = queue.DRIVERS['amqp']['channel'] +else: + listening_channel = 'default' + + +class BaseQueueDriver(BaseDriver, HasColoredCommands): + + def add_to_failed_queue_table(self, payload): + from config.database import DB as schema + if schema.get_schema_builder().has_table('failed_jobs'): + schema.table('failed_jobs').insert({ + 'driver': 'amqp', + 'channel': listening_channel, + 'payload': pickle.dumps(payload), + 'failed_at': pendulum.now() + }) + + def run_failed_jobs(self): + from config.database import DB as schema + try: + self.success('Attempting to send failed jobs back to the queue ...') + for job in schema.table('failed_jobs').get(): + payload = pickle.loads(job.payload) + schema.table('failed_jobs').where('payload', job.payload).delete() + self.push(payload['obj'], args=payload['args'], callback=payload['callback']) + except Exception: + 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, fair=False): + 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/masonite/drivers/QueueAmqpDriver.py b/masonite/drivers/QueueAmqpDriver.py index 4e8756f3e..76c235ba5 100644 --- a/masonite/drivers/QueueAmqpDriver.py +++ b/masonite/drivers/QueueAmqpDriver.py @@ -1,13 +1,17 @@ """ Driver for AMQP support """ +import inspect import pickle -import threading +import time + +import pendulum from config import queue from masonite.contracts import QueueContract -from masonite.drivers import BaseDriver -from masonite.app import App +from masonite.drivers import BaseQueueDriver from masonite.exceptions import DriverLibraryNotFound +from masonite.helpers import HasColoredCommands +from masonite.queues import Queueable if 'amqp' in queue.DRIVERS: listening_channel = queue.DRIVERS['amqp']['channel'] @@ -15,19 +19,46 @@ listening_channel = 'default' -class QueueAmqpDriver(QueueContract, BaseDriver): +class QueueAmqpDriver(BaseQueueDriver, QueueContract, HasColoredCommands): def __init__(self): """Queue AMQP Driver + """ + + # 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 + )) + + def push(self, *objects, args=(), callback='handle', ran=1, channel=None): + """Push objects onto the amqp stack. Arguments: - Container {masonite.app.App} -- The application container. + objects {*args of objects} - This can be several objects as parameters into this method. """ + if channel: + self.publishing_channel = channel - # Start the connection - self._connect() + for obj in objects: + # Publish to the channel for each object + payload = {'obj': obj, 'args': args, 'callback': callback, 'created': pendulum.now(), 'ran': ran} + try: + self._publish(payload) + except self.pika.exceptions.ConnectionClosed: + self.connect() + self._publish(payload) - def _connect(self): + def connect(self): try: import pika self.pika = pika @@ -35,42 +66,60 @@ def _connect(self): raise DriverLibraryNotFound( "Could not find the 'pika' library. Run pip install pika to fix this.") - connection = pika.BlockingConnection(pika.URLParameters('amqp://{}:{}@{}{}/{}'.format( + self.connection = pika.BlockingConnection(pika.URLParameters('amqp://{}:{}@{}{}/{}'.format( queue.DRIVERS['amqp']['username'], queue.DRIVERS['amqp']['password'], queue.DRIVERS['amqp']['host'], - ':' - + str(queue.DRIVERS['amqp']['port']) if 'port' in queue.DRIVERS['amqp'] and queue.DRIVERS['amqp']['port'] else '', + ':' + str(queue.DRIVERS['amqp']['port']) if 'port' in queue.DRIVERS['amqp'] and queue.DRIVERS['amqp']['port'] else '', queue.DRIVERS['amqp']['vhost'] if 'vhost' in queue.DRIVERS['amqp'] and queue.DRIVERS['amqp']['vhost'] else '%2F' ))) - # Get the channel - self.channel = connection.channel() + self.channel = self.connection.channel() - # Declare what queue we are working with - self.channel.queue_declare(queue=listening_channel, durable=True) + self.channel.queue_declare(queue=self.publishing_channel, durable=True) - def _publish(self, body): - self.channel.basic_publish(exchange='', - routing_key=listening_channel, - body=pickle.dumps( - body - ), - properties=self.pika.BasicProperties( - delivery_mode=2, # make message persistent - )) + return self - def push(self, *objects, args=(), callback='handle'): - """Push objects onto the amqp stack. + def consume(self, channel, fair=False): + self.success('[*] Waiting to process jobs on the "{}" channel. To exit press CTRL+C'.format( + channel)) - Arguments: - objects {*args of objects} - This can be several objects as parameters into this method. - """ + self.channel.basic_consume(self.work, + queue=channel) - for obj in objects: - # Publish to the channel for each object + if fair: + self.channel.basic_qos(prefetch_count=1) + return self.channel.start_consuming() + + def work(self, ch, method, properties, body): + from wsgi import container + job = pickle.loads(body) + obj = job['obj'] + args = job['args'] + callback = job['callback'] + ran = job['ran'] + + try: try: - self._publish({'obj': obj, 'args': args, 'callback': callback}) - except self.pika.exceptions.ConnectionClosed: - self._connect() - self._publish({'obj': obj, 'args': args}) + if inspect.isclass(obj): + obj = container.resolve(obj) + + getattr(obj, callback)(*args) + + except AttributeError: + obj(*args) + + self.success('[\u2713] Job Successfully Processed') + except Exception as e: + self.danger('Job Failed: {}'.format(str(e))) + + if ran < 3 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) + + ch.basic_ack(delivery_tag=method.delivery_tag) diff --git a/masonite/drivers/QueueAsyncDriver.py b/masonite/drivers/QueueAsyncDriver.py index e6a5da043..dc8c85f87 100644 --- a/masonite/drivers/QueueAsyncDriver.py +++ b/masonite/drivers/QueueAsyncDriver.py @@ -1,14 +1,14 @@ """Async Driver Method.""" -import threading import inspect +import threading -from masonite.contracts import QueueContract -from masonite.drivers import BaseDriver from masonite.app import App +from masonite.contracts import QueueContract +from masonite.drivers import BaseQueueDriver -class QueueAsyncDriver(QueueContract, BaseDriver): +class QueueAsyncDriver(BaseQueueDriver, QueueContract): """Queue Aysnc Driver.""" def __init__(self, app: App): @@ -19,7 +19,7 @@ def __init__(self, app: App): """ self.container = app - def push(self, *objects, args=(), callback='handle'): + def push(self, *objects, args=(), callback='handle', ran=1, channel=None): """Push objects onto the async stack. Arguments: diff --git a/masonite/drivers/__init__.py b/masonite/drivers/__init__.py index 915ac1cb0..dff62c55a 100644 --- a/masonite/drivers/__init__.py +++ b/masonite/drivers/__init__.py @@ -1,6 +1,7 @@ from .BaseDriver import BaseDriver from .BaseMailDriver import BaseMailDriver from .BaseUploadDriver import BaseUploadDriver +from .BaseQueueDriver import BaseQueueDriver from .BaseCacheDriver import BaseCacheDriver from .BroadcastAblyDriver import BroadcastAblyDriver from .BroadcastPusherDriver import BroadcastPusherDriver diff --git a/masonite/helpers/__init__.py b/masonite/helpers/__init__.py index 5a286f1fd..ee4378731 100644 --- a/masonite/helpers/__init__.py +++ b/masonite/helpers/__init__.py @@ -1,7 +1,8 @@ from .static import static from .password import password from .validator import validate -from .misc import random_string, dot, clean_request_input +from .misc import random_string, dot, clean_request_input, HasColoredCommands from .Extendable import Extendable from .time import cookie_expire_time from .structures import config, Dot +from .migrations import has_unmigrated_migrations diff --git a/masonite/helpers/filesystem.py b/masonite/helpers/filesystem.py index 01d801d86..5148c3968 100644 --- a/masonite/helpers/filesystem.py +++ b/masonite/helpers/filesystem.py @@ -1,4 +1,5 @@ import os +import shutil def make_directory(directory): @@ -10,3 +11,11 @@ def make_directory(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) + shutil.copyfile(file_path, os.path.join(os.getcwd(), to, datetime.datetime.utcnow().strftime("%Y_%m_%d_%H%M%S") + '_' + os.path.basename(directory_file))) diff --git a/masonite/helpers/migrations.py b/masonite/helpers/migrations.py new file mode 100644 index 000000000..255b3d143 --- /dev/null +++ b/masonite/helpers/migrations.py @@ -0,0 +1,33 @@ +import subprocess +from masonite.helpers import config + + +def has_unmigrated_migrations(): + if not config('application.debug'): + return False + + from wsgi import container + from config.database import DB + try: + DB.connection() + except Exception: + return False + + migration_directory = ['databases/migrations'] + for key, value in container.providers.items(): + if type(key) == str and 'MigrationDirectory' in key: + migration_directory.append(value) + + for directory in migration_directory: + try: + output = bytes(subprocess.check_output( + ['orator', 'migrate:status', '-c', + 'config/database.py', '-p', directory] + )).decode('utf-8') + + if 'No' in output: + return True + except Exception: + pass + + return False diff --git a/masonite/helpers/misc.py b/masonite/helpers/misc.py index 0aa30d8e8..6092b6920 100644 --- a/masonite/helpers/misc.py +++ b/masonite/helpers/misc.py @@ -52,3 +52,15 @@ def clean_request_input(value): 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)) diff --git a/masonite/helpers/time.py b/masonite/helpers/time.py index 6779354d0..8f51f308c 100644 --- a/masonite/helpers/time.py +++ b/masonite/helpers/time.py @@ -12,7 +12,7 @@ def cookie_expire_time(str_time): Returns: pendlum -- Returns Pendulum instance """ - if str_time is not 'expired': + if str_time != 'expired': number = int(str_time.split(" ")[0]) length = str_time.split(" ")[1] diff --git a/masonite/info.py b/masonite/info.py index 3bbc6a36b..69a9b04da 100644 --- a/masonite/info.py +++ b/masonite/info.py @@ -1,3 +1,3 @@ """Module for specifying the Masonite version in a central location.""" -VERSION = '2.1.14' +VERSION = '2.1.15' diff --git a/masonite/managers/Manager.py b/masonite/managers/Manager.py index 95e872b49..ab6dd3313 100644 --- a/masonite/managers/Manager.py +++ b/masonite/managers/Manager.py @@ -60,7 +60,8 @@ def create_driver(self, driver=None): UnacceptableDriverType -- Raised when a driver passed in is not a string or a class DriverNotFound -- Raised when the driver can not be found. """ - if not driver: + + if driver in (None, 'default'): driver = self.container.make(self.config).DRIVER.capitalize() else: if isinstance(driver, str): diff --git a/masonite/providers/AppProvider.py b/masonite/providers/AppProvider.py index c8339bd1b..2e7a35a99 100644 --- a/masonite/providers/AppProvider.py +++ b/masonite/providers/AppProvider.py @@ -9,7 +9,7 @@ MigrateRefreshCommand, MigrateResetCommand, MigrateRollbackCommand, MigrateStatusCommand, ModelCommand, ModelDocstringCommand, - ProviderCommand, QueueWorkCommand, + ProviderCommand, QueueWorkCommand, QueueTableCommand, RoutesCommand, SeedCommand, SeedRunCommand, ServeCommand, TinkerCommand, UpCommand, ValidatorCommand, ViewCommand) @@ -72,6 +72,7 @@ def _load_commands(self): self.app.bind('MasoniteModelDocstringCommand', ModelDocstringCommand()) self.app.bind('MasoniteProviderCommand', ProviderCommand()) self.app.bind('MasoniteQueueWorkCommand', QueueWorkCommand()) + self.app.bind('MasoniteQueueTableCommand', QueueTableCommand()) self.app.bind('MasoniteViewCommand', ViewCommand()) self.app.bind('MasoniteRoutesCommand', RoutesCommand()) self.app.bind('MasoniteServeCommand', ServeCommand()) diff --git a/masonite/request.py b/masonite/request.py index 49d29c609..981baad52 100644 --- a/masonite/request.py +++ b/masonite/request.py @@ -787,7 +787,11 @@ def compile_route_to_url(self, route, params={}): # if the url contains a parameter variable like @id:int if '@' in url: url = url.replace('@', '').split(':')[0] - compiled_url += str(params[url]) + '/' + if isinstance(params, dict): + compiled_url += str(params[url]) + '/' + elif isinstance(params, list): + compiled_url += str(params[0]) + '/' + del params[0] else: compiled_url += url + '/' diff --git a/masonite/response.py b/masonite/response.py index 878ba56fc..5dc0ca9bd 100644 --- a/masonite/response.py +++ b/masonite/response.py @@ -6,6 +6,9 @@ from masonite.helpers.Extendable import Extendable from masonite.view import View +from orator.support.collection import Collection +from orator import Model + class Response(Extendable): @@ -87,6 +90,8 @@ def view(self, view, status=200): if isinstance(view, dict) or isinstance(view, list): return self.json(view) + elif isinstance(view, Collection) or isinstance(view, Model): + view = self.json(view.serialize()) elif isinstance(view, int): view = str(view) elif isinstance(view, View): diff --git a/masonite/routes.py b/masonite/routes.py index 5fd91d63d..307b1de4a 100644 --- a/masonite/routes.py +++ b/masonite/routes.py @@ -250,7 +250,7 @@ def has_required_domain(self): Returns: bool """ - if self.request.has_subdomain() and (self.required_domain is '*' or self.request.subdomain == self.required_domain): + if self.request.has_subdomain() and (self.required_domain == '*' or self.request.subdomain == self.required_domain): return True return False @@ -360,25 +360,29 @@ def compile_route_to_regex(self, router): class Get(BaseHttpRoute): """Class for specifying GET requests.""" - def __init__(self): + def __init__(self, route=None, output=None): """Get constructor.""" self.method_type = ['GET'] self.list_middleware = [] + if route and output: + self.route(route, output) class Post(BaseHttpRoute): """Class for specifying POST requests.""" - def __init__(self): + def __init__(self, route=None, output=None): """Post constructor.""" self.method_type = ['POST'] self.list_middleware = [] + if route and output: + self.route(route, output) class Match(BaseHttpRoute): """Class for specifying POST requests.""" - def __init__(self, method_type=['GET']): + def __init__(self, method_type=['GET'], route=None, output=None): """Post constructor.""" if not isinstance(method_type, list): raise RouteException("Method type needs to be a list. Got '{}'".format(method_type)) @@ -386,33 +390,41 @@ def __init__(self, method_type=['GET']): # Make all method types in list uppercase self.method_type = [x.upper() for x in method_type] self.list_middleware = [] + if route and output: + self.route(route, output) class Put(BaseHttpRoute): """Class for specifying PUT requests.""" - def __init__(self): + def __init__(self, route=None, output=None): """Put constructor.""" self.method_type = ['PUT'] self.list_middleware = [] + if route and output: + self.route(route, output) class Patch(BaseHttpRoute): """Class for specifying Patch requests.""" - def __init__(self): + def __init__(self, route=None, output=None): """Patch constructor.""" self.method_type = ['PATCH'] self.list_middleware = [] + if route and output: + self.route(route, output) class Delete(BaseHttpRoute): """Class for specifying Delete requests.""" - def __init__(self): + def __init__(self, route=None, output=None): """Delete constructor.""" self.method_type = ['DELETE'] self.list_middleware = [] + if route and output: + self.route(route, output) class ViewRoute(BaseHttpRoute): diff --git a/masonite/snippets/migrations/create_failed_jobs_table.py b/masonite/snippets/migrations/create_failed_jobs_table.py new file mode 100644 index 000000000..72ef12560 --- /dev/null +++ b/masonite/snippets/migrations/create_failed_jobs_table.py @@ -0,0 +1,22 @@ +from orator.migrations import Migration + + +class CreateFailedJobsTable(Migration): + + def up(self): + """ + Run the migrations. + """ + with self.schema.create('failed_jobs') as table: + table.increments('id') + table.string('driver') + table.string('channel') + table.binary('payload') + table.timestamp('failed_at') + table.timestamps() + + def down(self): + """ + Revert the migrations. + """ + self.schema.drop('failed_jobs') diff --git a/routes/web.py b/routes/web.py index e63bc5b65..d0c683d47 100644 --- a/routes/web.py +++ b/routes/web.py @@ -5,6 +5,7 @@ ROUTES = [ Get().route('/test', None).middleware('auth'), + Get().route('/queue', 'TestController@queue'), Redirect('/redirect', 'test'), Get().domain('test').route('/test', None).middleware('auth'), Get().domain('test').route('/unit/test', 'TestController@testing').middleware('auth'), diff --git a/tests/helpers/test_config.py b/tests/helpers/test_config.py index f1fe76cca..94737b1ab 100644 --- a/tests/helpers/test_config.py +++ b/tests/helpers/test_config.py @@ -1,6 +1,7 @@ import pydoc from masonite.helpers import config, Dot +from config import database class TestConfig: @@ -21,10 +22,10 @@ def test_dict_dot_returns_value(self): assert Dot().dict_dot('s3.test', {'s3': {'test': 'value'}}) == 'value' def test_config_can_get_dict_value_inside_dict(self): - assert self.config('database.DATABASES.default') == 'sqlite' + assert self.config('database.DATABASES.default') == database.DATABASES['default'] def test_config_can_get_dict_value_inside_dict_with_lowercase(self): - assert self.config('database.databases.default') == 'sqlite' + assert self.config('database.databases.default') == database.DATABASES['default'] def test_config_can_get_dict_inside_dict_inside_dict(self): assert isinstance(self.config('database.databases.sqlite'), dict) diff --git a/tests/queues/test_drivers.py b/tests/queues/test_drivers.py index 5a55b65bc..1bafe21db 100644 --- a/tests/queues/test_drivers.py +++ b/tests/queues/test_drivers.py @@ -38,7 +38,9 @@ def setup_method(self): self.app.bind('Queueable', Queueable) self.app.bind('Container', self.app) self.app.bind('QueueManager', QueueManager(self.app)) + self.app.bind('Queue', QueueManager(self.app).driver(self.app.make('QueueConfig').DRIVER)) self.drivers = ['async'] + if env('RUN_AMQP'): self.drivers.append('amqp') @@ -53,3 +55,8 @@ def test_async_driver_can_run_any_callback_method(self): def test_async_driver_can_run_any_method(self): for driver in self.drivers: assert self.app.make('QueueManager').driver(driver).push(Random().send) is None + + def test_should_return_default_driver(self): + assert isinstance(self.app.make('Queue'), QueueAsyncDriver) + assert isinstance(self.app.make('Queue').driver('async'), QueueAsyncDriver) + assert isinstance(self.app.make('Queue').driver('default'), QueueAsyncDriver) diff --git a/tests/test_requests.py b/tests/test_requests.py index d1aaaccf3..2c204a753 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -263,6 +263,7 @@ def test_request_route_returns_url(self): assert request.route('test.url') == '/test/url' assert request.route('test.id', {'id': 1}) == '/test/url/1' + assert request.route('test.id', [1]) == '/test/url/1' def test_request_redirection(self): app = App() @@ -327,6 +328,21 @@ def test_redirect_compiles_url_with_parameters(self): assert request.compile_route_to_url(route, params) == '/test/1' + def test_redirect_compiles_url_with_list_parameters(self): + app = App() + app.bind('Request', self.request) + request = app.make('Request').load_app(app) + + route = 'test/@id' + params = ['1'] + + assert request.compile_route_to_url(route, params) == '/test/1' + + route = 'test/@id/@user/test/@slug' + params = ['1', '2', '3'] + + assert request.compile_route_to_url(route, params) == '/test/1/2/test/3' + def test_redirect_compiles_url_with_multiple_parameters(self): app = App() app.bind('Request', self.request) diff --git a/tests/test_response.py b/tests/test_response.py index 84b9bb038..a7d2b8e45 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -5,6 +5,25 @@ from masonite.app import App from app.http.controllers.TestController import TestController as ControllerTest +from orator import Model +from orator.support.collection import Collection + +class MockUser(Model): + + + + def all(self): + return Collection([ + {'name': 'TestUser', 'email': 'user@email.com'}, + {'name': 'TestUser', 'email': 'user@email.com'} + ]) + + def find(self, id): + self.name = 'TestUser' + self.email = 'user@email.com' + return self + + class TestResponse: def setup_method(self): @@ -13,7 +32,8 @@ def setup_method(self): self.app.bind('Request', self.request) self.app.bind('StatusCode', '404 Not Found') self.response = Response(self.app) - + self.app.bind('Response', self.response) + def test_can_set_json(self): self.response.json({'test': 'value'}) @@ -54,3 +74,17 @@ def test_view_can_set_own_status_code(self): self.response.view(self.app.resolve(ControllerTest().change_status)) assert self.request.is_status(203) + + def test_view_should_return_a_json_response_when_retrieve_a_user_from_model(self): + + assert isinstance(MockUser(), Model) + self.response.view(MockUser().all()) + + json_response = '[{"name": "TestUser", "email": "user@email.com"}, {"name": "TestUser", "email": "user@email.com"}]' + assert self.app.make('Response') == json_response + + self.response.view(MockUser().find(1)) + + json_response = '{"name": "TestUser", "email": "user@email.com"}' + assert self.app.make('Response') == json_response + diff --git a/tests/test_routes.py b/tests/test_routes.py index 05df4f871..91c07be80 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -66,6 +66,23 @@ def test_route_gets_controllers(self): def test_route_doesnt_break_on_incorrect_controller(self): assert Get().route('test/url', 'BreakController@show') + def test_route_can_pass_route_values_in_constructor(self): + route = Get('test/url', 'BreakController@show') + assert route.route_url == 'test/url' + route = Post('test/url', 'BreakController@show') + assert route.route_url == 'test/url' + route = Put('test/url', 'BreakController@show') + assert route.route_url == 'test/url' + route = Patch('test/url', 'BreakController@show') + assert route.route_url == 'test/url' + route = Delete('test/url', 'BreakController@show') + assert route.route_url == 'test/url' + + def test_route_can_pass_route_values_in_constructor_and_use_middleware(self): + route = Get('test/url', 'BreakController@show').middleware('auth') + assert route.route_url == 'test/url' + assert route.list_middleware == ['auth'] + def test_route_gets_deeper_module_controller(self): route = Get().route('test/url', 'subdirectory.SubController@show') assert route.controller