From 7773011c11a530d4fecb3a554b9da543db4658de Mon Sep 17 00:00:00 2001 From: androiddrew Date: Sun, 19 May 2019 19:32:26 -0400 Subject: [PATCH] initial commit --- .gitignore | 68 ++++ README.md | 25 ++ .../mongo-quickstart-logo-scaled.jpg | Bin 0 -> 56274 bytes sample_data/readme.md | 9 + sample_data/snake_bnb.zip | Bin 0 -> 2100 bytes src/snake_bnb/requirements.txt | 5 + src/snake_bnb/src/data/bookings.py | 18 + src/snake_bnb/src/data/cages.py | 22 ++ src/snake_bnb/src/data/mongo_setup.py | 5 + src/snake_bnb/src/data/owners.py | 16 + src/snake_bnb/src/data/readme.md | 1 + src/snake_bnb/src/data/snakes.py | 16 + src/snake_bnb/src/infrastructure/state.py | 12 + .../src/infrastructure/switchlang.py | 108 ++++++ src/snake_bnb/src/program.py | 65 ++++ src/snake_bnb/src/program_guests.py | 171 +++++++++ src/snake_bnb/src/program_hosts.py | 217 +++++++++++ src/snake_bnb/src/services/data_service.py | 147 ++++++++ src/snake_bnb/src/services/readme.md | 1 + src/starter_code_snake_bnb/requirements.txt | 5 + src/starter_code_snake_bnb/src/data/readme.md | 1 + .../src/infrastructure/state.py | 10 + .../src/infrastructure/switchlang.py | 108 ++++++ src/starter_code_snake_bnb/src/program.py | 64 ++++ .../src/program_guests.py | 88 +++++ .../src/program_hosts.py | 131 +++++++ .../src/services/readme.md | 1 + .../1-welcome/1-welcome_transcript_final.txt | 118 ++++++ .../1-intro-to-mongodb_transcript_final.txt | 87 +++++ .../2-how-doc-dbs-work_transcript_final.txt | 72 ++++ .../3-who-uses-mongodb_transcript_final.txt | 66 ++++ ...eling-vs-doc-modeling_transcript_final.txt | 94 +++++ ...2-modeling-guidelines_transcript_final.txt | 152 ++++++++ ...ntegration-vs-app-dbs_transcript_final.txt | 62 ++++ ...ing-demo-starter-code_transcript_final.txt | 157 ++++++++ .../1-how-odms-work_transcript_final.txt | 69 ++++ ...-intro-to-mongoengine_transcript_final.txt | 32 ++ ...10-demo-register-cage_transcript_final.txt | 208 +++++++++++ ...o-add-a-bookable-time_transcript_final.txt | 121 +++++++ ...-demo-managing-snakes_transcript_final.txt | 86 +++++ .../13-demo-book-a-cage_transcript_final.txt | 338 ++++++++++++++++++ ...iew-bookings-as-guest_transcript_final.txt | 137 +++++++ ...view-bookings-as-host_transcript_final.txt | 120 +++++++ .../16-concept-inserting_transcript_final.txt | 30 ++ .../17-concept-queries_transcript_final.txt | 22 ++ ...querying-subdocuments_transcript_final.txt | 33 ++ ...pt-querying-operators_transcript_final.txt | 19 + ...ept-updating-via-docs_transcript_final.txt | 37 ++ ...pdating-via-operators_transcript_final.txt | 57 +++ ...-register-connections_transcript_final.txt | 83 +++++ ...-register-connections_transcript_final.txt | 51 +++ ...-mongoengine-entities_transcript_final.txt | 156 ++++++++ ...-mongoengine-entities_transcript_final.txt | 91 +++++ ...7-demo-create-account_transcript_final.txt | 187 ++++++++++ .../8-demo-robo-3t_transcript_final.txt | 32 ++ .../9-demo-login_transcript_final.txt | 38 ++ .../1-youve-done-it_transcript_final.txt | 11 + .../2-get-the-code_transcript_final.txt | 24 ++ .../3-full-course_transcript_final.txt | 61 ++++ 59 files changed, 4165 insertions(+) create mode 100644 .gitignore create mode 100755 README.md create mode 100755 readme_resources/mongo-quickstart-logo-scaled.jpg create mode 100755 sample_data/readme.md create mode 100755 sample_data/snake_bnb.zip create mode 100755 src/snake_bnb/requirements.txt create mode 100755 src/snake_bnb/src/data/bookings.py create mode 100755 src/snake_bnb/src/data/cages.py create mode 100755 src/snake_bnb/src/data/mongo_setup.py create mode 100755 src/snake_bnb/src/data/owners.py create mode 100755 src/snake_bnb/src/data/readme.md create mode 100755 src/snake_bnb/src/data/snakes.py create mode 100755 src/snake_bnb/src/infrastructure/state.py create mode 100755 src/snake_bnb/src/infrastructure/switchlang.py create mode 100755 src/snake_bnb/src/program.py create mode 100755 src/snake_bnb/src/program_guests.py create mode 100755 src/snake_bnb/src/program_hosts.py create mode 100755 src/snake_bnb/src/services/data_service.py create mode 100755 src/snake_bnb/src/services/readme.md create mode 100755 src/starter_code_snake_bnb/requirements.txt create mode 100755 src/starter_code_snake_bnb/src/data/readme.md create mode 100755 src/starter_code_snake_bnb/src/infrastructure/state.py create mode 100755 src/starter_code_snake_bnb/src/infrastructure/switchlang.py create mode 100755 src/starter_code_snake_bnb/src/program.py create mode 100755 src/starter_code_snake_bnb/src/program_guests.py create mode 100755 src/starter_code_snake_bnb/src/program_hosts.py create mode 100755 src/starter_code_snake_bnb/src/services/readme.md create mode 100755 video_transcripts/1-welcome/1-welcome_transcript_final.txt create mode 100755 video_transcripts/2-why-nosql-and-mongodb/1-intro-to-mongodb_transcript_final.txt create mode 100755 video_transcripts/2-why-nosql-and-mongodb/2-how-doc-dbs-work_transcript_final.txt create mode 100755 video_transcripts/2-why-nosql-and-mongodb/3-who-uses-mongodb_transcript_final.txt create mode 100755 video_transcripts/3-modeling-documents/1-relational-modeling-vs-doc-modeling_transcript_final.txt create mode 100755 video_transcripts/3-modeling-documents/2-modeling-guidelines_transcript_final.txt create mode 100755 video_transcripts/3-modeling-documents/3-integration-vs-app-dbs_transcript_final.txt create mode 100755 video_transcripts/3-modeling-documents/4-getting-demo-starter-code_transcript_final.txt create mode 100755 video_transcripts/4-mongoengine/1-how-odms-work_transcript_final.txt create mode 100755 video_transcripts/4-mongoengine/2-intro-to-mongoengine_transcript_final.txt create mode 100755 video_transcripts/5-building-with-mongoengine/10-demo-register-cage_transcript_final.txt create mode 100755 video_transcripts/5-building-with-mongoengine/11-demo-add-a-bookable-time_transcript_final.txt create mode 100755 video_transcripts/5-building-with-mongoengine/12-demo-managing-snakes_transcript_final.txt create mode 100755 video_transcripts/5-building-with-mongoengine/13-demo-book-a-cage_transcript_final.txt create mode 100755 video_transcripts/5-building-with-mongoengine/14-demo-view-bookings-as-guest_transcript_final.txt create mode 100755 video_transcripts/5-building-with-mongoengine/15-demo-view-bookings-as-host_transcript_final.txt create mode 100755 video_transcripts/5-building-with-mongoengine/16-concept-inserting_transcript_final.txt create mode 100755 video_transcripts/5-building-with-mongoengine/17-concept-queries_transcript_final.txt create mode 100755 video_transcripts/5-building-with-mongoengine/18-concept-querying-subdocuments_transcript_final.txt create mode 100755 video_transcripts/5-building-with-mongoengine/19-concept-querying-operators_transcript_final.txt create mode 100755 video_transcripts/5-building-with-mongoengine/20-concept-updating-via-docs_transcript_final.txt create mode 100755 video_transcripts/5-building-with-mongoengine/21-concept-updating-via-operators_transcript_final.txt create mode 100755 video_transcripts/5-building-with-mongoengine/3-register-connections_transcript_final.txt create mode 100755 video_transcripts/5-building-with-mongoengine/4-concept-register-connections_transcript_final.txt create mode 100755 video_transcripts/5-building-with-mongoengine/5-mongoengine-entities_transcript_final.txt create mode 100755 video_transcripts/5-building-with-mongoengine/6-concept-mongoengine-entities_transcript_final.txt create mode 100755 video_transcripts/5-building-with-mongoengine/7-demo-create-account_transcript_final.txt create mode 100755 video_transcripts/5-building-with-mongoengine/8-demo-robo-3t_transcript_final.txt create mode 100755 video_transcripts/5-building-with-mongoengine/9-demo-login_transcript_final.txt create mode 100755 video_transcripts/6-conclusion/1-youve-done-it_transcript_final.txt create mode 100755 video_transcripts/6-conclusion/2-get-the-code_transcript_final.txt create mode 100755 video_transcripts/6-conclusion/3-full-course_transcript_final.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..46c7c11 --- /dev/null +++ b/.gitignore @@ -0,0 +1,68 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Pycharm +.idea + +# Mongo +/data + +# Mac +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000..7a0efbf --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# MongoDB Quickstart with Python course + +This project follow's Michael C. Kennedy's [MongoDB Quickstart.](https://github.com/mikeckennedy/mongodb-quickstart-course) + +## Docker instructions + +The docker container and documentation can be found [here](https://hub.docker.com/_/mongo). + +We will use the following command to start up a container to host our data and + +``` +docker run --name quick-mongo -p 27017:27017 -v /Users/Drewbednar/PycharmProjects/try_mongo/data:/data/db -d mongo:3.6.12-xenial +``` + +When looking to restart your container simply run: + +``` +docker start quick-mongo +``` + +To check the configuration for the container run: + +``` +docker inspect quick-mongo +``` \ No newline at end of file diff --git a/readme_resources/mongo-quickstart-logo-scaled.jpg b/readme_resources/mongo-quickstart-logo-scaled.jpg new file mode 100755 index 0000000000000000000000000000000000000000..5733e7ba0db3ecc19e1ddbc4a8abdc60dbf7a5a5 GIT binary patch literal 56274 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 range: + if start >= stop: + raise ValueError("Start must be less than stop.") + + return range(start, stop + step, step) \ No newline at end of file diff --git a/src/snake_bnb/src/program.py b/src/snake_bnb/src/program.py new file mode 100755 index 0000000..2ca37cf --- /dev/null +++ b/src/snake_bnb/src/program.py @@ -0,0 +1,65 @@ +from colorama import Fore +import program_guests +import program_hosts +import data.mongo_setup as mongo_setup + + +def main(): + mongo_setup.global_init() + + print_header() + + try: + while True: + if find_user_intent() == 'book': + program_guests.run() + else: + program_hosts.run() + except KeyboardInterrupt: + return + + +def print_header(): + snake = \ + """ + ~8I?? OM + M..I?Z 7O?M + ? ?8 ?I8 + MOM???I?ZO??IZ + M:??O??????MII + OIIII$NI7??I$ + IIID?IIZ + +$ ,IM ,~7??I7$ +I? MM ?:::?7$ +?? 7,::?778+=~+??8 +??Z ?,:,:I7$I??????+~~+ +??D N==7,::,I77??????????=~$ +~??? I~~I?,::,77$Z????????????? +???+~M $+~+???? :::II7$II777II??????N +OI??????????I$$M=,:+7??I$7I??????????? + N$$$ZDI =++:$???????????II78 + =~~:~~7II777$$Z + ~ZMM~ """ + + print(Fore.WHITE + '**************** SNAKE BnB ****************') + print(Fore.GREEN + snake) + print(Fore.WHITE + '*********************************************') + print() + print("Welcome to Snake BnB!") + print("Why are you here?") + print() + + +def find_user_intent(): + print("[g] Book a cage for your snake") + print("[h] Offer extra cage space") + print() + choice = input("Are you a [g]uest or [h]ost? ") + if choice == 'h': + return 'offer' + + return 'book' + + +if __name__ == '__main__': + main() diff --git a/src/snake_bnb/src/program_guests.py b/src/snake_bnb/src/program_guests.py new file mode 100755 index 0000000..7548c86 --- /dev/null +++ b/src/snake_bnb/src/program_guests.py @@ -0,0 +1,171 @@ +import datetime +from dateutil import parser + +from infrastructure.switchlang import switch +import program_hosts as hosts +import services.data_service as svc +from program_hosts import success_msg, error_msg +import infrastructure.state as state + + +def run(): + print(' ****************** Welcome guest **************** ') + print() + + show_commands() + + while True: + action = hosts.get_action() + + with switch(action) as s: + s.case('c', hosts.create_account) + s.case('l', hosts.log_into_account) + + s.case('a', add_a_snake) + s.case('y', view_your_snakes) + s.case('b', book_a_cage) + s.case('v', view_bookings) + s.case('m', lambda: 'change_mode') + + s.case('?', show_commands) + s.case('', lambda: None) + s.case(['x', 'bye', 'exit', 'exit()'], hosts.exit_app) + + s.default(hosts.unknown_command) + + state.reload_account() + + if action: + print() + + if s.result == 'change_mode': + return + + +def show_commands(): + print('What action would you like to take:') + print('[C]reate an account') + print('[L]ogin to your account') + print('[B]ook a cage') + print('[A]dd a snake') + print('View [y]our snakes') + print('[V]iew your bookings') + print('[M]ain menu') + print('e[X]it app') + print('[?] Help (this info)') + print() + + +def add_a_snake(): + print(' ****************** Add a snake **************** ') + if not state.active_account: + error_msg("You must log in first to add a snake") + return + + name = input("What is your snake's name? ") + if not name: + error_msg('cancelled') + return + + length = float(input('How long is your snake (in meters)? ')) + species = input("Species? ") + is_venomous = input("Is your snake venomous [y]es, [n]o? ").lower().startswith('y') + + snake = svc.add_snake(state.active_account, name, length, species, is_venomous) + state.reload_account() + success_msg('Created {} with id {}'.format(snake.name, snake.id)) + + +def view_your_snakes(): + print(' ****************** Your snakes **************** ') + if not state.active_account: + error_msg("You must log in first to view your snakes") + return + + snakes = svc.get_snakes_for_user(state.active_account.id) + print("You have {} snakes.".format(len(snakes))) + for s in snakes: + print(" * {} is a {} that is {}m long and is {}venomous.".format( + s.name, + s.species, + s.length, + '' if s.is_venomous else 'not ' + )) + + +def book_a_cage(): + print(' ****************** Book a cage **************** ') + if not state.active_account: + error_msg("You must log in first to book a cage") + return + + snakes = svc.get_snakes_for_user(state.active_account.id) + if not snakes: + error_msg('You must first [a]dd a snake before you can book a cage.') + return + + print("Let's start by finding available cages.") + start_text = input("Check-in date [yyyy-mm-dd]: ") + if not start_text: + error_msg('cancelled') + return + + checkin = parser.parse( + start_text + ) + checkout = parser.parse( + input("Check-out date [yyyy-mm-dd]: ") + ) + if checkin >= checkout: + error_msg('Check in must be before check out') + return + + print() + for idx, s in enumerate(snakes): + print('{}. {} (length: {}, venomous: {})'.format( + idx + 1, + s.name, + s.length, + 'yes' if s.is_venomous else 'no' + )) + + snake = snakes[int(input('Which snake do you want to book (number)')) - 1] + + cages = svc.get_available_cages(checkin, checkout, snake) + + print("There are {} cages available in that time.".format(len(cages))) + for idx, c in enumerate(cages): + print(" {}. {} with {}m carpeted: {}, has toys: {}.".format( + idx + 1, + c.name, + c.square_meters, + 'yes' if c.is_carpeted else 'no', + 'yes' if c.has_toys else 'no')) + + if not cages: + error_msg("Sorry, no cages are available for that date.") + return + + cage = cages[int(input('Which cage do you want to book (number)')) - 1] + svc.book_cage(state.active_account, snake, cage, checkin, checkout) + + success_msg('Successfully booked {} for {} at ${}/night.'.format(cage.name, snake.name, cage.price)) + + +def view_bookings(): + print(' ****************** Your bookings **************** ') + if not state.active_account: + error_msg("You must log in first to register a cage") + return + + snakes = {s.id: s for s in svc.get_snakes_for_user(state.active_account.id)} + bookings = svc.get_bookings_for_user(state.active_account.email) + + print("You have {} bookings.".format(len(bookings))) + for b in bookings: + print(' * Snake: {} is booked at {} from {} for {} days.'.format( + snakes.get(b.guest_snake_id).name, + b.cage.name, + datetime.date(b.check_in_date.year, b.check_in_date.month, b.check_in_date.day), + (b.check_out_date - b.check_in_date).days + )) diff --git a/src/snake_bnb/src/program_hosts.py b/src/snake_bnb/src/program_hosts.py new file mode 100755 index 0000000..cabf764 --- /dev/null +++ b/src/snake_bnb/src/program_hosts.py @@ -0,0 +1,217 @@ +import datetime +from colorama import Fore +from dateutil import parser + +from infrastructure.switchlang import switch +import infrastructure.state as state +import services.data_service as svc + + +def run(): + print(' ****************** Welcome host **************** ') + print() + + show_commands() + + while True: + action = get_action() + + with switch(action) as s: + s.case('c', create_account) + s.case('a', create_account) + s.case('l', log_into_account) + s.case('y', list_cages) + s.case('r', register_cage) + s.case('u', update_availability) + s.case('v', view_bookings) + s.case('m', lambda: 'change_mode') + s.case(['x', 'bye', 'exit', 'exit()'], exit_app) + s.case('?', show_commands) + s.case('', lambda: None) + s.default(unknown_command) + + if action: + print() + + if s.result == 'change_mode': + return + + +def show_commands(): + print('What action would you like to take:') + print('[C]reate an [a]ccount') + print('[L]ogin to your account') + print('List [y]our cages') + print('[R]egister a cage') + print('[U]pdate cage availability') + print('[V]iew your bookings') + print('Change [M]ode (guest or host)') + print('e[X]it app') + print('[?] Help (this info)') + print() + + +def create_account(): + print(' ****************** REGISTER **************** ') + + name = input('What is your name? ') + email = input('What is your email? ').strip().lower() + + old_account = svc.find_account_by_email(email) + if old_account: + error_msg(f"ERROR: Account with email {email} already exists.") + return + + state.active_account = svc.create_account(name, email) + success_msg(f"Created new account with id {state.active_account.id}.") + + +def log_into_account(): + print(' ****************** LOGIN **************** ') + + email = input('What is your email? ').strip().lower() + account = svc.find_account_by_email(email) + + if not account: + error_msg(f'Could not find account with email {email}.') + return + + state.active_account = account + success_msg('Logged in successfully.') + + +def register_cage(): + print(' ****************** REGISTER CAGE **************** ') + + if not state.active_account: + error_msg('You must login first to register a cage.') + return + + meters = input('How many square meters is the cage? ') + if not meters: + error_msg('Cancelled') + return + + meters = float(meters) + carpeted = input("Is it carpeted [y, n]? ").lower().startswith('y') + has_toys = input("Have snake toys [y, n]? ").lower().startswith('y') + allow_dangerous = input("Can you host venomous snakes [y, n]? ").lower().startswith('y') + name = input("Give your cage a name: ") + price = float(input("How much are you charging? ")) + + cage = svc.register_cage( + state.active_account, name, + allow_dangerous, has_toys, carpeted, meters, price + ) + + state.reload_account() + success_msg(f'Register new cage with id {cage.id}.') + + +def list_cages(suppress_header=False): + if not suppress_header: + print(' ****************** Your cages **************** ') + + if not state.active_account: + error_msg('You must login first to register a cage.') + return + + cages = svc.find_cages_for_user(state.active_account) + print(f"You have {len(cages)} cages.") + for idx, c in enumerate(cages): + print(f' {idx+1}. {c.name} is {c.square_meters} meters.') + for b in c.bookings: + print(' * Booking: {}, {} days, booked? {}'.format( + b.check_in_date, + (b.check_out_date - b.check_in_date).days, + 'YES' if b.booked_date is not None else 'no' + )) + + +def update_availability(): + print(' ****************** Add available date **************** ') + + if not state.active_account: + error_msg("You must log in first to register a cage") + return + + list_cages(suppress_header=True) + + cage_number = input("Enter cage number: ") + if not cage_number.strip(): + error_msg('Cancelled') + print() + return + + cage_number = int(cage_number) + + cages = svc.find_cages_for_user(state.active_account) + selected_cage = cages[cage_number - 1] + + success_msg("Selected cage {}".format(selected_cage.name)) + + start_date = parser.parse( + input("Enter available date [yyyy-mm-dd]: ") + ) + days = int(input("How many days is this block of time? ")) + + svc.add_available_date( + selected_cage, + start_date, + days + ) + + success_msg(f'Date added to cage {selected_cage.name}.') + + +def view_bookings(): + print(' ****************** Your bookings **************** ') + + if not state.active_account: + error_msg("You must log in first to register a cage") + return + + cages = svc.find_cages_for_user(state.active_account) + + bookings = [ + (c, b) + for c in cages + for b in c.bookings + if b.booked_date is not None + ] + + print("You have {} bookings.".format(len(bookings))) + for c, b in bookings: + print(' * Cage: {}, booked date: {}, from {} for {} days.'.format( + c.name, + datetime.date(b.booked_date.year, b.booked_date.month, b.booked_date.day), + datetime.date(b.check_in_date.year, b.check_in_date.month, b.check_in_date.day), + b.duration_in_days + )) + + +def exit_app(): + print() + print('bye') + raise KeyboardInterrupt() + + +def get_action(): + text = '> ' + if state.active_account: + text = f'{state.active_account.name}> ' + + action = input(Fore.YELLOW + text + Fore.WHITE) + return action.strip().lower() + + +def unknown_command(): + print("Sorry we didn't understand that command.") + + +def success_msg(text): + print(Fore.LIGHTGREEN_EX + text + Fore.WHITE) + + +def error_msg(text): + print(Fore.LIGHTRED_EX + text + Fore.WHITE) diff --git a/src/snake_bnb/src/services/data_service.py b/src/snake_bnb/src/services/data_service.py new file mode 100755 index 0000000..e745fd1 --- /dev/null +++ b/src/snake_bnb/src/services/data_service.py @@ -0,0 +1,147 @@ +from typing import List + +import datetime + +import bson + +from data.bookings import Booking +from data.cages import Cage +from data.owners import Owner +from data.snakes import Snake + + +def create_account(name: str, email: str) -> Owner: + owner = Owner() + owner.name = name + owner.email = email + + owner.save() + + return owner + + +def find_account_by_email(email: str) -> Owner: + owner = Owner.objects(email=email).first() + return owner + + +def register_cage(active_account: Owner, + name, allow_dangerous, has_toys, + carpeted, meters, price) -> Cage: + cage = Cage() + + cage.name = name + cage.square_meters = meters + cage.is_carpeted = carpeted + cage.has_toys = has_toys + cage.allow_dangerous_snakes = allow_dangerous + cage.price = price + + cage.save() + + account = find_account_by_email(active_account.email) + account.cage_ids.append(cage.id) + account.save() + + return cage + + +def find_cages_for_user(account: Owner) -> List[Cage]: + query = Cage.objects(id__in=account.cage_ids) + cages = list(query) + + return cages + + +def add_available_date(cage: Cage, + start_date: datetime.datetime, days: int) -> Cage: + booking = Booking() + booking.check_in_date = start_date + booking.check_out_date = start_date + datetime.timedelta(days=days) + + cage = Cage.objects(id=cage.id).first() + cage.bookings.append(booking) + cage.save() + + return cage + + +def add_snake(account, name, length, species, is_venomous) -> Snake: + snake = Snake() + snake.name = name + snake.length = length + snake.species = species + snake.is_venomous = is_venomous + snake.save() + + owner = find_account_by_email(account.email) + owner.snake_ids.append(snake.id) + owner.save() + + return snake + + +def get_snakes_for_user(user_id: bson.ObjectId) -> List[Snake]: + owner = Owner.objects(id=user_id).first() + snakes = Snake.objects(id__in=owner.snake_ids).all() + + return list(snakes) + + +def get_available_cages(checkin: datetime.datetime, + checkout: datetime.datetime, snake: Snake) -> List[Cage]: + min_size = snake.length / 4 + + query = Cage.objects() \ + .filter(square_meters__gte=min_size) \ + .filter(bookings__check_in_date__lte=checkin) \ + .filter(bookings__check_out_date__gte=checkout) + + if snake.is_venomous: + query = query.filter(allow_dangerous_snakes=True) + + cages = query.order_by('price', '-square_meters') + + final_cages = [] + for c in cages: + for b in c.bookings: + if b.check_in_date <= checkin and b.check_out_date >= checkout and b.guest_snake_id is None: + final_cages.append(c) + + return final_cages + + +def book_cage(account, snake, cage, checkin, checkout): + booking: Booking = None + + for b in cage.bookings: + if b.check_in_date <= checkin and b.check_out_date >= checkout and b.guest_snake_id is None: + booking = b + break + + booking.guest_owner_id = account.id + booking.guest_snake_id = snake.id + booking.booked_date = datetime.datetime.now() + + cage.save() + + +def get_bookings_for_user(email: str) -> List[Booking]: + account = find_account_by_email(email) + + booked_cages = Cage.objects() \ + .filter(bookings__guest_owner_id=account.id) \ + .only('bookings', 'name') + + def map_cage_to_booking(cage, booking): + booking.cage = cage + return booking + + bookings = [ + map_cage_to_booking(cage, booking) + for cage in booked_cages + for booking in cage.bookings + if booking.guest_owner_id == account.id + ] + + return bookings diff --git a/src/snake_bnb/src/services/readme.md b/src/snake_bnb/src/services/readme.md new file mode 100755 index 0000000..a7f1060 --- /dev/null +++ b/src/snake_bnb/src/services/readme.md @@ -0,0 +1 @@ +We'll put our data access code and logic here. \ No newline at end of file diff --git a/src/starter_code_snake_bnb/requirements.txt b/src/starter_code_snake_bnb/requirements.txt new file mode 100755 index 0000000..11d3b39 --- /dev/null +++ b/src/starter_code_snake_bnb/requirements.txt @@ -0,0 +1,5 @@ +pymongo +mongoengine +tqdm +colorama +python-dateutil diff --git a/src/starter_code_snake_bnb/src/data/readme.md b/src/starter_code_snake_bnb/src/data/readme.md new file mode 100755 index 0000000..9a566d3 --- /dev/null +++ b/src/starter_code_snake_bnb/src/data/readme.md @@ -0,0 +1 @@ +We'll put some data things in this folder when we start the course. \ No newline at end of file diff --git a/src/starter_code_snake_bnb/src/infrastructure/state.py b/src/starter_code_snake_bnb/src/infrastructure/state.py new file mode 100755 index 0000000..26a4b30 --- /dev/null +++ b/src/starter_code_snake_bnb/src/infrastructure/state.py @@ -0,0 +1,10 @@ +active_account = None + + +def reload_account(): + global active_account + if not active_account: + return + + # TODO: pull owner account from the database. + pass diff --git a/src/starter_code_snake_bnb/src/infrastructure/switchlang.py b/src/starter_code_snake_bnb/src/infrastructure/switchlang.py new file mode 100755 index 0000000..bda6ea9 --- /dev/null +++ b/src/starter_code_snake_bnb/src/infrastructure/switchlang.py @@ -0,0 +1,108 @@ +import uuid +from typing import Callable, Any + + +class switch: + """ + python-switch is a module-level implementation of the switch statement for Python. + See https://github.com/mikeckennedy/python-switch for full details. + Copyright Michael Kennedy (https://twitter.com/mkennedy) + """ + __no_result = uuid.uuid4() + __default = uuid.uuid4() + + def __init__(self, value): + self.value = value + self.cases = set() + self._found = False + self.__result = switch.__no_result + self._falling_through = False + self._func_stack = [] + + def default(self, func: Callable[[], Any]): + """ + Use as option final statement in switch block. + + with switch(val) as s: + s.case(...) + s.case(...) + s.default(function) + + :param func: Any callable taking no parameters to be executed if this (default) case matches. + :return: None + """ + self.case(switch.__default, func) + + def case(self, key, func: Callable[[], Any], fallthrough=False): + """ + Specify a case for the switch block: + + with switch(val) as s: + s.case('a', function) + s.case('b', function, fallthrough=True) + s.default(function) + + :param key: Key for the case test (if this is a list or range, the items will each be added as a case) + :param func: Any callable taking no parameters to be executed if this case matches. + :param fallthrough: Optionally fall through to the subsequent case (defaults to False) + :return: + """ + if fallthrough is not None: + if self._falling_through: + self._func_stack.append(func) + if not fallthrough: + self._falling_through = False + + if isinstance(key, list) or isinstance(key, range): + found = False + for i in key: + if self.case(i, func, fallthrough=None): + found = True + if fallthrough is not None: + self._falling_through = fallthrough + return found + + if key in self.cases: + raise ValueError("Duplicate case: {}".format(key)) + if not func: + raise ValueError("Action for case cannot be None.") + if not callable(func): + raise ValueError("Func must be callable.") + + self.cases.add(key) + if key == self.value or not self._found and key == self.__default: + self._func_stack.append(func) + self._found = True + if fallthrough is not None: + self._falling_through = fallthrough + return True + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_val: + raise exc_val + + if not self._func_stack: + raise Exception("Value does not match any case and there " + "is no default case: value {}".format(self.value)) + + for func in self._func_stack: + # noinspection PyCallingNonCallable + self.__result = func() + + @property + def result(self): + if self.__result == switch.__no_result: + raise Exception("No result has been computed (did you access " + "switch.result inside the with block?)") + + return self.__result + + +def closed_range(start: int, stop: int, step=1) -> range: + if start >= stop: + raise ValueError("Start must be less than stop.") + + return range(start, stop + step, step) \ No newline at end of file diff --git a/src/starter_code_snake_bnb/src/program.py b/src/starter_code_snake_bnb/src/program.py new file mode 100755 index 0000000..4eeb2a0 --- /dev/null +++ b/src/starter_code_snake_bnb/src/program.py @@ -0,0 +1,64 @@ +from colorama import Fore +import program_guests +import program_hosts + + +def main(): + # TODO: Setup mongoengine global values + + print_header() + + try: + while True: + if find_user_intent() == 'book': + program_guests.run() + else: + program_hosts.run() + except KeyboardInterrupt: + return + + +def print_header(): + snake = \ + """ + ~8I?? OM + M..I?Z 7O?M + ? ?8 ?I8 + MOM???I?ZO??IZ + M:??O??????MII + OIIII$NI7??I$ + IIID?IIZ + +$ ,IM ,~7??I7$ +I? MM ?:::?7$ +?? 7,::?778+=~+??8 +??Z ?,:,:I7$I??????+~~+ +??D N==7,::,I77??????????=~$ +~??? I~~I?,::,77$Z????????????? +???+~M $+~+???? :::II7$II777II??????N +OI??????????I$$M=,:+7??I$7I??????????? + N$$$ZDI =++:$???????????II78 + =~~:~~7II777$$Z + ~ZMM~ """ + + print(Fore.WHITE + '**************** SNAKE BnB ****************') + print(Fore.GREEN + snake) + print(Fore.WHITE + '*********************************************') + print() + print("Welcome to Snake BnB!") + print("Why are you here?") + print() + + +def find_user_intent(): + print("[g] Book a cage for your snake") + print("[h] Offer extra cage space") + print() + choice = input("Are you a [g]uest or [h]ost? ") + if choice == 'h': + return 'offer' + + return 'book' + + +if __name__ == '__main__': + main() diff --git a/src/starter_code_snake_bnb/src/program_guests.py b/src/starter_code_snake_bnb/src/program_guests.py new file mode 100755 index 0000000..ee8f953 --- /dev/null +++ b/src/starter_code_snake_bnb/src/program_guests.py @@ -0,0 +1,88 @@ +from infrastructure.switchlang import switch +import program_hosts as hosts +import infrastructure.state as state + + +def run(): + print(' ****************** Welcome guest **************** ') + print() + + show_commands() + + while True: + action = hosts.get_action() + + with switch(action) as s: + s.case('c', hosts.create_account) + s.case('l', hosts.log_into_account) + + s.case('a', add_a_snake) + s.case('y', view_your_snakes) + s.case('b', book_a_cage) + s.case('v', view_bookings) + s.case('m', lambda: 'change_mode') + + s.case('?', show_commands) + s.case('', lambda: None) + s.case(['x', 'bye', 'exit', 'exit()'], hosts.exit_app) + + s.default(hosts.unknown_command) + + state.reload_account() + + if action: + print() + + if s.result == 'change_mode': + return + + +def show_commands(): + print('What action would you like to take:') + print('[C]reate an account') + print('[L]ogin to your account') + print('[B]ook a cage') + print('[A]dd a snake') + print('View [y]our snakes') + print('[V]iew your bookings') + print('[M]ain menu') + print('e[X]it app') + print('[?] Help (this info)') + print() + + +def add_a_snake(): + print(' ****************** Add a snake **************** ') + # TODO: Require an account + # TODO: Get snake info from user + # TODO: Create the snake in the DB. + + print(" -------- NOT IMPLEMENTED -------- ") + + +def view_your_snakes(): + print(' ****************** Your snakes **************** ') + + # TODO: Require an account + # TODO: Get snakes from DB, show details list + + print(" -------- NOT IMPLEMENTED -------- ") + + +def book_a_cage(): + print(' ****************** Book a cage **************** ') + # TODO: Require an account + # TODO: Verify they have a snake + # TODO: Get dates and select snake + # TODO: Find cages available across date range + # TODO: Let user select cage to book. + + print(" -------- NOT IMPLEMENTED -------- ") + + +def view_bookings(): + print(' ****************** Your bookings **************** ') + # TODO: Require an account + # TODO: List booking info along with snake info + + print(" -------- NOT IMPLEMENTED -------- ") diff --git a/src/starter_code_snake_bnb/src/program_hosts.py b/src/starter_code_snake_bnb/src/program_hosts.py new file mode 100755 index 0000000..bd7e34a --- /dev/null +++ b/src/starter_code_snake_bnb/src/program_hosts.py @@ -0,0 +1,131 @@ +from colorama import Fore +from infrastructure.switchlang import switch +import infrastructure.state as state + + +def run(): + print(' ****************** Welcome host **************** ') + print() + + show_commands() + + while True: + action = get_action() + + with switch(action) as s: + s.case('c', create_account) + s.case('a', log_into_account) + s.case('l', list_cages) + s.case('r', register_cage) + s.case('u', update_availability) + s.case('v', view_bookings) + s.case('m', lambda: 'change_mode') + s.case(['x', 'bye', 'exit', 'exit()'], exit_app) + s.case('?', show_commands) + s.case('', lambda: None) + s.default(unknown_command) + + if action: + print() + + if s.result == 'change_mode': + return + + +def show_commands(): + print('What action would you like to take:') + print('[C]reate an account') + print('Login to your [a]ccount') + print('[L]ist your cages') + print('[R]egister a cage') + print('[U]pdate cage availability') + print('[V]iew your bookings') + print('Change [M]ode (guest or host)') + print('e[X]it app') + print('[?] Help (this info)') + print() + + +def create_account(): + print(' ****************** REGISTER **************** ') + # TODO: Get name & email + # TODO: Create account, set as logged in. + + print(" -------- NOT IMPLEMENTED -------- ") + + +def log_into_account(): + print(' ****************** LOGIN **************** ') + + # TODO: Get email + # TODO: Find account in DB, set as logged in. + + print(" -------- NOT IMPLEMENTED -------- ") + + +def register_cage(): + print(' ****************** REGISTER CAGE **************** ') + + # TODO: Require an account + # TODO: Get info about cage + # TODO: Save cage to DB. + + print(" -------- NOT IMPLEMENTED -------- ") + + +def list_cages(supress_header=False): + if not supress_header: + print(' ****************** Your cages **************** ') + + # TODO: Require an account + # TODO: Get cages, list details + + print(" -------- NOT IMPLEMENTED -------- ") + + +def update_availability(): + print(' ****************** Add available date **************** ') + + # TODO: Require an account + # TODO: list cages + # TODO: Choose cage + # TODO: Set dates, save to DB. + + print(" -------- NOT IMPLEMENTED -------- ") + + +def view_bookings(): + print(' ****************** Your bookings **************** ') + + # TODO: Require an account + # TODO: Get cages, and nested bookings as flat list + # TODO: Print details for each + + print(" -------- NOT IMPLEMENTED -------- ") + + +def exit_app(): + print() + print('bye') + raise KeyboardInterrupt() + + +def get_action(): + text = '> ' + if state.active_account: + text = f'{state.active_account.name}> ' + + action = input(Fore.YELLOW + text + Fore.WHITE) + return action.strip().lower() + + +def unknown_command(): + print("Sorry we didn't understand that command.") + + +def success_msg(text): + print(Fore.LIGHTGREEN_EX + text + Fore.WHITE) + + +def error_msg(text): + print(Fore.LIGHTRED_EX + text + Fore.WHITE) diff --git a/src/starter_code_snake_bnb/src/services/readme.md b/src/starter_code_snake_bnb/src/services/readme.md new file mode 100755 index 0000000..a7f1060 --- /dev/null +++ b/src/starter_code_snake_bnb/src/services/readme.md @@ -0,0 +1 @@ +We'll put our data access code and logic here. \ No newline at end of file diff --git a/video_transcripts/1-welcome/1-welcome_transcript_final.txt b/video_transcripts/1-welcome/1-welcome_transcript_final.txt new file mode 100755 index 0000000..260657e --- /dev/null +++ b/video_transcripts/1-welcome/1-welcome_transcript_final.txt @@ -0,0 +1,118 @@ +00:01 Hello and welcome to MongoDB Quick Start with Python. +00:05 MongoDB is one of the most popular +00:07 and exciting database technologies around. +00:09 Python is one of the most popular +00:11 and fastest growing language there is +00:12 and these two technologies work great together +00:15 and that's exactly what this course is about. +00:17 We're going to quickly get started with MongoDB, +00:20 write some code against it, +00:22 and build some realistic applications. +00:24 The goal with this class is to teach you MongoDB. +00:27 We assume you know a little bit of Python +00:29 but you'll of course pick some things up along the way +00:31 if you don't know it already, and really, +00:33 we want to get you going quick, hence the name, Quick Start. +00:36 Let's get started by talking about +00:38 what we're going to cover in this course. +00:39 We're going to focus on three main things. +00:41 We're going to start with why do you care about +00:44 NoSQL and Document Databases. +00:46 How do document databases make working with schemas easier, +00:50 modeling data easier, as well as add performance +00:54 and flexibility to our applications. +00:55 We're going to talk about modeling specifically, +00:58 'cause it is one of the things that is +00:59 pretty challenging about document databases. +01:02 If you come from a relational database world, +01:04 you probably know about third normal form. +01:06 This is a way to carefully and +01:08 more structured way to plan out how you model your data. +01:11 In document databases, you don't really have that. +01:13 There's a lot more sort of flexibility +01:16 in how you design things. +01:18 This is great for you once you understand it +01:20 and get really good at it, but it's challenging to know +01:22 how to model things when you get started +01:24 because there's so much flexibility +01:26 and it's kind of open-ended. +01:27 It feels a little more like art than science, in some sense. +01:30 So we're going to focus specifically on +01:32 techniques and trade offs, and some guidelines +01:35 I have to come up with to help you be successful +01:38 modeling your data with document databases such as MongoDB. +01:42 Then we're going to start writing code for real. +01:44 We're going to use a Mongo ODM, Object Document Mapper. +01:49 Think of SQL Alchemy but for document databases. +01:51 They're called MongoEngine, and we're going to use that +01:54 to create some classes to model our data, +01:57 and map those classes to and from MongoDB +01:59 and use that as the foundation of our application. +02:02 Speaking of applications, +02:03 what are we going to build in this class? +02:05 Well, we're going to build an AirBnB clone but for snakes. +02:09 Okay, so we're going to build Snake BnB +02:11 and this allows you, when you're traveling with your snake, +02:14 your pet snake, you don't want it to have to +02:17 live out in the car or something like that. +02:19 You want to get it a cage that it can live in, +02:21 a proper snake cage where it'll be happy. +02:23 There'll be owners of cages +02:25 who can put their cages up for rent, +02:27 and snake owners, pet owners +02:29 who want to travel with their pets and +02:31 have their pet have a place to stay. +02:33 We're going to create this sort of silly +02:35 AirBnB knockoff clone, but we're going to model +02:37 many of the operations you would see +02:39 in real AirBnB on our application here +02:42 so it's going to be pretty rich in terms of data. +02:44 As far as tools go, well of course, +02:45 we're going to talk about MongoDB, right. +02:47 That's the database we're using, +02:49 but you're also going to learn some other things +02:50 that are pretty awesome in the course. +02:52 We're going to talk about MongoEngine. +02:54 This is the primary library +02:55 we're going to use to talk to MongoDB. +02:57 It's built upon another one that's very popular +03:00 that has sort of the lowest level +03:01 official way to talk to MongoDB called PyMongo, +03:04 so you might see a little bit of +03:05 both of those actually in the course. +03:07 We're going to use what I think is +03:08 the best tool for accessing MongoDB, +03:11 something that used to be called Robomongo +03:14 but now it's called Robo 3T, +03:16 'cause it was acquired by a company called 3T. +03:18 This is part command line, +03:20 part GUI way to interact with MongoDB, +03:23 and it's beautiful, and free open source, it's great. +03:26 Also, we're going to use PyCharm for our Python code. +03:29 You don't have to use PyCharm for this course, +03:31 but you'll see me using it, and I think you'll see +03:33 a lot of benefits as we go. +03:36 Speaking of me, who am I anyway? +03:37 Who is this voice that you're listening to? +03:39 Hi, my name is Michael Kennedy. +03:40 You can find me on Twitter at @mkennedy. +03:43 What makes me qualified to teach this course? +03:45 Well, first of all, I host the most popular Python podcast +03:48 called Talk Python to Me, and I've interviewed +03:51 many, many people, including some of the folks from MongoDB, +03:54 the company as well as authors who've written about +03:57 MongoDB design patterns and things like that, +03:59 so I've had a lot of experience +04:00 working with people from Python and MongoDB. +04:03 I've created the Talk Python Training Company +04:06 and written many Python courses +04:08 and MongoDB courses there as well. +04:10 And also, I am part of the MongoDB Masters Program. +04:14 This is a group of about 35 external community members +04:18 who give advice back to MongoDB, +04:21 and I've been part of this for many years, +04:23 worked closely with the folks inside MongoDB over the years. +04:26 So this is what you have in store for you, +04:28 lots of awesome MongoDB and Python. +04:30 I hope you're excited. +04:31 Let's get right to it. diff --git a/video_transcripts/2-why-nosql-and-mongodb/1-intro-to-mongodb_transcript_final.txt b/video_transcripts/2-why-nosql-and-mongodb/1-intro-to-mongodb_transcript_final.txt new file mode 100755 index 0000000..269246d --- /dev/null +++ b/video_transcripts/2-why-nosql-and-mongodb/1-intro-to-mongodb_transcript_final.txt @@ -0,0 +1,87 @@ +00:01 Let's begin by looking at why you might choose MongoDB +00:05 over other NoSQL databases, other document databases, +00:09 or even other relational databases. +00:12 I'm sure you've heard of MongoDB. +00:14 That's why you're taking this course. +00:15 But how popular is it relative to other databases? +00:19 Is it really the right choice? +00:20 Well, let's look at some data from 2017. +00:23 If you compare MongoDB against other NoSQL databases, +00:27 Cassandra, CouchDB, RavenDB, things like that. +00:31 You will find one of these databases is unlike the others. +00:34 Look at that, that's incredible, +00:36 how much more popular MongoDB is over these. +00:39 It's not just the popularity that it's five, +00:42 10 times, maybe 50 times more popular than RavenDB. +00:46 That's great. +00:47 That's very important. +00:47 But also, the trend. +00:50 These others are either flat or turning downwards. +00:51 And this is, besides the little blip here, +00:54 based on probably the end of the year numbers +00:57 or something like that. +00:58 At the end of the data, this is a really big deal. +01:01 This is incredible how much growth is here, +01:04 and it's still going up. +01:05 MongoDB is really, really popular +01:07 in terms of a database that people are using. +01:11 And that's great because that means it's well-tested. +01:14 When we get the section a little bit later, +01:16 we'll look at some of the users of MongoDB. +01:18 But it's really important +01:19 that there are some heavy workloads put onto these databases +01:23 that you're going to depend upon. +01:24 And if it can take what those people are doing, +01:26 surely, it can take what you have to throw at it as well. +01:29 Now, MongoDB is also loved. +01:31 If you look at Stack Overflow, +01:33 at their 2017 Developer's Survey, +01:36 and you look at the most loved databases: +01:38 these are databases that the developers are using currently +01:43 and how they feel about it. +01:44 We see MongoDB ranks right near the top. +01:46 So, definitely 55% of the people +01:49 who are using MongoDB love it compared to +01:51 say SQLite or Cassandra. +01:54 So, this is pretty good. +01:55 But what's even more interesting +01:56 is that it's the most wanted database. +01:59 These are technologies that you're not currently +02:01 able to work with but you would like to. +02:03 So, there are tons of people that want to work with MongoDB +02:05 but for whatever reason, they've got some Legacy system +02:07 built on MySQL or whatever, they don't get the chance to. +02:11 All these pieces of data tell you +02:13 MongoDB is a really good technology to have +02:15 in your tool belt. +02:16 And it's easy to get ahold of. +02:18 MongoDB is open source and it's free. +02:20 You can just go to github.com/mongodb/mongo +02:23 and clone it and it's right there. +02:25 You can see it has 11,000 stars, 3,000 forks. +02:29 And this screenshot I took here was updated two hours ago. +02:32 It's under very, very active development. +02:34 It's a live and vibrant project. +02:36 Finally, if you actually want to get MongoDB, +02:38 don't go to GitHub. +02:40 GitHub is cool, it's great. +02:41 You have the source but don't go there. +02:42 What you really want to do is you want +02:43 to go to mongodb.com/download-center. +02:47 Or just go to mongodb.com and click Download. +02:49 It'll take you here. +02:50 And you download here, you get it as binary. +02:53 So, if you're on Mac, +02:54 I recommend you use Homebrew to install it. +02:56 But you can also download a tarball. +02:58 If you're on Windows, get the MSI. +03:00 And on Linux, you can actually install it +03:02 with aptitude or some package manager like that. +03:05 In this course, we're not going to go into the details +03:07 of setting up MongoDB. +03:08 Just follow the instructions. +03:09 In the full MongoDB course you have, +03:11 we walk through all the steps, +03:12 but we just want to get started. +03:13 This the quick start. +03:14 So, let's keep moving. diff --git a/video_transcripts/2-why-nosql-and-mongodb/2-how-doc-dbs-work_transcript_final.txt b/video_transcripts/2-why-nosql-and-mongodb/2-how-doc-dbs-work_transcript_final.txt new file mode 100755 index 0000000..8ea79b3 --- /dev/null +++ b/video_transcripts/2-why-nosql-and-mongodb/2-how-doc-dbs-work_transcript_final.txt @@ -0,0 +1,72 @@ +00:00 Let's look at one of these records stored in MongoDB +00:03 to see how document databases work. +00:05 Here we have a JSON record. +00:08 This is actually from one of my courses, +00:10 the Python Jumpstart by Building 10 Apps, +00:12 and this is how I represent it in the database. +00:14 We've got standard columnar type things, +00:17 so we have an ID, we have a title, a course ID, +00:20 duration in seconds, these types of things. +00:22 Now, first of all, you might be wondering, +00:24 wait a minute, JSON database? +00:26 JSON database, is that really efficient? +00:28 Well, probably not. +00:29 What MongoDB actually stores is a binary representation. +00:33 So like a binary tokenized version of this record, +00:37 but they call it BSON, +00:38 because it's like binary JSON. +00:40 But we're humans, we don't read binary, we read text. +00:43 So we're looking at the textual representation. +00:45 So this is not exactly how it gets stored, +00:46 but this is pretty close. +00:47 So we have these regular column type pieces of information, +00:51 ID, title, and so on, +00:53 but we also have this other thing, these lectures. +00:55 Now these lectures are in this chapter represented +00:58 by this record from this course, +00:59 and notice the bracket in JavaScript, +01:02 which basically this +01:03 means that it is an array. +01:06 And the array contains a bunch of sub-objects. +01:08 So this is really interesting. +01:09 Instead of just having a chapter table +01:12 and a lecture table, +01:13 and doing a join or something like that, +01:15 a form key relationship, +01:16 we're actually putting the lectures inside the same record. +01:19 This is pretty interesting. +01:21 When you look at it like this, +01:22 you can imagine that this is like a precomputed join. +01:25 If I do a query for the lecture, +01:28 say, given ID1001, +01:30 and I get this record back, it already has the lectures. +01:33 I don't have to make another round trip +01:35 to the database to get them. +01:36 I don't have to do a join against several tables +01:39 in that original query. +01:40 It's literally a primary key query +01:42 against an indexed primary key and is insanely fast, +01:45 yet it already comes back +01:46 with all this extra information. +01:48 So this is really cool. +01:49 If I get the chapter, I have the lectures, great. +01:51 But, you might be wondering well, really, +01:54 what if I need to ask the question in reverse? +01:57 Like, fundamentally, if I need to get at lecture 10106, +02:02 will I be able to query MongoDB quickly and efficiently +02:06 to get that lecture? +02:08 And it turns out, the answer is yes. +02:10 And that's why document databases are awesome. +02:12 It's not just some nested blob stored in +02:15 the original record, +02:16 you can, as part of the query language +02:18 and part of indexes, traverse these hierarchies +02:21 in very, very rich and powerful ways. +02:23 We don't lose very much query capability +02:26 just by putting lectures in this one record. +02:29 So this is really neat, +02:30 and this is sort of the foundational, +02:32 most important take away from document databases. +02:35 We store them in these flexible JSON type of objects, +02:38 and we can nest additional things like lists of numbers, +02:43 or even subdocuments, as we have in this case. diff --git a/video_transcripts/2-why-nosql-and-mongodb/3-who-uses-mongodb_transcript_final.txt b/video_transcripts/2-why-nosql-and-mongodb/3-who-uses-mongodb_transcript_final.txt new file mode 100755 index 0000000..0201939 --- /dev/null +++ b/video_transcripts/2-why-nosql-and-mongodb/3-who-uses-mongodb_transcript_final.txt @@ -0,0 +1,66 @@ +00:02 Before we move on, +00:03 let's take a moment and look at who uses MongoDB. +00:06 Programming languages and databases +00:09 and technologies aren't necessarily popularity contests. +00:13 Just because something's popular +00:14 doesn't necessarily mean that it's great: +00:17 evidence, PHP for example, +00:19 or VB, or something like this. +00:20 Technologies are popular and some of them are great, +00:23 and sometimes great technologies are popular. +00:25 There are some important things that come along +00:28 with being popular and used by big important companies. +00:32 That means these things are durable, +00:34 tested, and have really been through the ringer. +00:36 MongoDB has been used by some really big customers +00:40 and some really interesting use cases. +00:42 I know some of the biggest ones +00:44 are not on this page even. +00:46 They're not listed here. +00:46 So we're going to take a quick tour of a couple of customers +00:49 who are using MongoDB +00:50 and we'll look at how they're using it. +00:52 Okay, so the first one that I want to look at, +00:54 scroll down here, you can see names that you might know. +00:56 Some cool stuff here. +00:57 Let's check out Royal Bank of Scotland. +00:58 Banks are supposed to be conservative. +01:01 Things like this, they probably wouldn't use weird +01:03 NoSQL document databases, +01:04 they're going to stick to their traditional Oracle +01:06 or single server, or whatever. +01:07 Well, if we look at Royal Bank of Scotland, +01:09 they're using MongoDB +01:11 to support a global enterprise data service +01:14 underpinning several core trading systems. +01:16 If you're a bank, the core trading systems are +01:18 pretty much the center of the universe, right? +01:22 So the fact that they're driving that with Mongo, +01:23 and that's high performance, +01:25 and it's doing that for them, +01:26 that's really awesome. +01:26 Let's check out Expedia. +01:27 Expedia, they have their app built on MongoDB, +01:30 and they are supporting millions of customers +01:32 shopping for flights, hotels, cars, things like that. +01:37 That's pretty awesome. +01:38 Let's check out another one down here. +01:39 EA, so video games. +01:41 This is the world's best-selling sports game franchise, +01:43 and they rely on MongoDB to scale the millions of players. +01:47 That is really awesome. +01:48 One more, before we move on. +01:49 These guys down here a little farther, +01:52 that's SailThru. +01:54 SailThru is a company that does outbound mail +01:57 and analytics and things like that. +01:58 They are a marketing company +02:01 very much doing tons of email type of stuff, like I said. +02:04 And they have over 40 TB of data +02:06 and a 120 physical, mostly physical, nodes. +02:10 So 120 servers all working, running MongoDB, +02:16 in some giant cluster, which is a pretty awesome use case. +02:20 And, of course, you have things like Shutterfly +02:21 running on MongoDB, Squarespace, on and on. +02:24 There's a ton of cool use cases down here, +02:26 but I think you've got the idea. diff --git a/video_transcripts/3-modeling-documents/1-relational-modeling-vs-doc-modeling_transcript_final.txt b/video_transcripts/3-modeling-documents/1-relational-modeling-vs-doc-modeling_transcript_final.txt new file mode 100755 index 0000000..4ca275b --- /dev/null +++ b/video_transcripts/3-modeling-documents/1-relational-modeling-vs-doc-modeling_transcript_final.txt @@ -0,0 +1,94 @@ +00:00 Are you ready to model some real applications +00:03 in MongoDB using documents? +00:05 It's time to get our hands dirty +00:06 and to really start building something. +00:07 In this chapter, we're going to go through +00:10 our SnakeBNB application, come up with the entities, +00:13 the classes and tables, or collections +00:16 as they're called in MongoDB, and model them out. +00:18 So we're going to first look at how this modeling +00:22 and document databases compares to traditional +00:24 third normal form modeling and relational databases. +00:27 We're going to use a pretty simple example +00:28 but I think you'll get the idea, +00:30 and you'll see it much more in action +00:33 when we build the real app. +00:34 Let's take this simple bookstore model here. +00:37 We have books, they have a bunch of properties. +00:39 We have people who publish those books named publishers, +00:41 they have a name, when they were founded, +00:43 and you can navigate this foreign key relationship +00:46 from publisher over to the publisher ID on the book. +00:49 Now, we also have a user, and a user might rate a book. +00:53 So we have users and we have ratings +00:55 and we have a foreign key relationship between them. +00:57 And then out from rating over to book +00:59 we have a one-to-many relationship there, right? +01:01 A book can have many ratings. +01:03 We have a couple of foreign key relationships +01:05 going on in this place. +01:07 Now let me tell you, in a real application, +01:08 there'd be many more little tables +01:11 with extra information like this. +01:12 Like ratings about books and so on, and +01:15 let's say reviews for example; things like that. +01:17 Maybe even related items that we've pre-computed +01:20 from some sort of machine learning, +01:22 but we want to store that in the database. +01:24 So imagine this model here having +01:26 15 tables with relationships +01:29 across all the various pieces back and forth. +01:31 I just want to keep it simple. +01:32 Fits on a screen, you're not going to go crazy with it. +01:34 So, how would we model this in MongoDB? +01:37 How would we model this using documents? +01:39 Well, you would see that it's somewhat simpler. +01:41 The more craziness that we had in a relational model, +01:44 the sort of more contrast you will see here. +01:46 So we still have our publisher and they have their ID +01:48 and when they were founded. +01:49 We have our user in the same columns, +01:51 or pieces of information there as well. +01:53 Same as book. +01:54 But now our ratings, we've decided, +01:56 when we get a book, most of the time +01:58 we actually want to know the rating. +01:59 We want to say this is a 3.4 star book, or it has 72 ratings. +02:04 Even in a list, we want to show that stuff. +02:06 So we're pretty sure we want to have +02:08 these ratings always with the books. +02:10 Why put 'em in a separate table? +02:12 Let's embed them. +02:12 Now we still have some relationships +02:14 like we had in the relational model. +02:16 For example, we have our publisher ID on books +02:18 and that links over to publisher. +02:20 Now, this is what, in MongoDB, I refer to as +02:22 a soft foreign key constraint. +02:24 If it was a relationship between books and publisher, +02:27 it's the publisher ID set to the ID of the publisher. +02:29 But the database itself doesn't enforce this, alright? +02:32 You need to be a little more careful in your app +02:34 and we'll see how we do that when we get to the code. +02:36 But, as I was saying about ratings, +02:37 these we're not going to put in a separate collection. +02:40 In fact, we're going to store those inside our books, +02:43 so we can embed these objects and arrays +02:46 of either straight values like numbers or strings, +02:49 or actual sub-documents like our ratings here. +02:52 So we might have rating one, two, three, and so on. +02:54 It's actually part of the record of the book. +02:56 So when we get the book record back, +02:57 we already have the ratings, things like that. +03:00 So again, we can think of these ratings +03:02 being embedded within books as a pre-computed join. +03:06 There's a slight bit of overhead +03:08 if you actually wanted the book without the ratings. +03:10 I mean, you're just going to get them back and ignore them +03:12 most of the time anyway, if that was the case. +03:14 Most of the time you do want the ratings. +03:16 this is a huge speed up. +03:18 And like I said, imagine there were 15 tables before +03:20 and five of them could be collapsed into the books. +03:23 Now you have a five-way join going down +03:24 to a single primary key query; that'd be amazing. diff --git a/video_transcripts/3-modeling-documents/2-modeling-guidelines_transcript_final.txt b/video_transcripts/3-modeling-documents/2-modeling-guidelines_transcript_final.txt new file mode 100755 index 0000000..e3afba9 --- /dev/null +++ b/video_transcripts/3-modeling-documents/2-modeling-guidelines_transcript_final.txt @@ -0,0 +1,152 @@ +00:00 As we discussed, modeling with documents and +00:03 document databases is little bit more art. +00:05 There's a little bit more flexibility and +00:07 kind of just gut-feel of how you should do it, +00:09 but let me give you some guidelines that will +00:12 give you a clear set of considerations +00:15 as you work on your data models. +00:17 You'll see that the primary question for working +00:20 with document databases is to embed or not to embed. +00:24 That is, when there's a relationship between +00:26 two things in your application. +00:28 Should one of those be a sub-document? +00:31 Should it be contained within the same record +00:33 as the thing it is related to? +00:34 Or should they be two separate collections, +00:36 (what MongoDB calls tables: collections), +00:39 because they're not tabular, right? +00:40 Should those be two separate collections, +00:42 but just relate to each other. +00:44 We're going to try to answer this question. +00:46 I'll try to provide you some guidelines +00:48 for answering this question. +00:49 The first one is: is the embedded data +00:52 wanted 80% of the time when you have the outer, +00:56 or other related object? +00:58 Let's go back to our example we just worked with. +01:00 We had a book and the book had ratings. +01:02 To make this concrete, the question here is +01:05 do we care about having information about the ratings +01:08 most of the time when we're working with books? +01:10 So if our website lists books, +01:13 and that listing has a number of ratings +01:16 and the average rating, things like that, +01:17 listed as part of the listing, +01:19 you pull up a book, maybe the ratings, +01:21 and the reviews are shown right there-- +01:22 most of the time, when we have the book, +01:24 we have the ratings involved somehow, +01:26 then we would want to embed the ratings within the book. +01:29 That's what we did in our data model. +01:30 We said, yes, +01:31 we do want the ratings most of the time. +01:34 Now, let's look at this in reverse. +01:36 How often do you want the embedded data +01:39 without the containing document? +01:41 So in the same example, how often is it the case +01:43 that you would like the ratings without the book? +01:49 Where would that show up in our apps? +01:50 So maybe you as a user, +01:52 go to your profile page in the bookstore, +01:55 and there you can see all the books you've rated, right? +01:57 And the details about the ratings. +01:58 You don't actually care about necessarily the books, +02:00 you just want, these are the ratings +02:02 that I've given to things, and here's my comment and so on. +02:05 You don't actually want most of the details +02:07 or maybe any of the details about the book itself. +02:09 So if you're in that sort of situation, +02:12 a lot of the time, you might want to put that +02:14 into a separate collection and not embed it. +02:16 You can still do it. +02:18 You can still do this query and it'll come back very quickly, +02:20 there's ways to work with it. +02:21 But you'll see that you have to do a query in the database +02:24 and then a little bit of filtering on the application side. +02:26 So it's not prohibitive, +02:31 it's not that you can't get the contained document +02:33 without its container, +02:34 but it's a little bit more clumsy. +02:36 So if this is something you frequently want to do, +02:39 then maybe consider not embedding it. +02:41 Now, is the embedded document a bounded set? +02:43 Let's look at ratings. +02:45 How many ratings might a book have? +02:47 10 ratings, 100 ratings, 1000 ratings. +02:49 Is that number going to just grow +02:51 The number of ratings that we have? +02:53 If this was page views and details about the browser, +02:57 IP address, and date and time of a page we viewed, +03:00 that would not make a good candidate for embedding that, +03:02 like say, for views of a book +03:04 because that could just grow and grow +03:05 as the popularity of your site grows. +03:07 And it could make the document so large +03:09 that when you retrieve it from the database, +03:10 actually the network traffic and the disk traffic +03:12 would be a problem. +03:13 I don't really see that happening with ratings. +03:15 I mean, even on Amazon, super popular books have +03:18 hundreds not millions of ratings. +03:20 So this is probably okay, but if it's an unbounded set, +03:23 you do not want to embed it. +03:25 And these have bounds small, right? +03:27 Maybe millions of views still are being recorded +03:31 within sign of a book would be a really bad idea. +03:33 And the reason is these documents are limited +03:35 to 16 megabytes. +03:37 So no single record of MongoDB can be larger +03:40 than 16 megabytes. +03:41 And this is not a limitation of MongoDB. +03:42 This is them trying to protect you from yourself. +03:45 You do not want to go and just say, query buy, say, ISBN, +03:49 and try to pull back a book +03:50 and actually read 100 megabytes off a disk in +03:53 over the network. +03:53 That would destroy the performance of your database. +03:56 So having these very, very large records is a problem. +03:59 So they actually set an upper bound on how large +04:02 that can be. +04:03 And that limit is right now, currently, +04:05 at the time of recording, 16 megabytes. +04:07 But you shouldn't think of 16 megabytes as like, +04:09 well, if it's 10 megabytes, everything's fine. +04:11 We still got a long ways to go. +04:13 No, you should try to keep these, +04:15 in the kilobytes: tens, twenties, hundreds of kilobytes, +04:18 not megabytes because that's going to really hurt your +04:21 database performance unless in some situation, +04:23 it just makes avton of sense to have these +04:25 very large documents. +04:25 So having small bounded sets means that your documents +04:29 won't grow into huge, huge monolithic things +04:32 that are hard to work with. +04:33 Also, how varied are your queries? +04:35 So one of the things that you do with document database is +04:38 is you try to structure the document to answer +04:41 the most common questions in the most well-structured way. +04:46 So if you're always going to say, I would like to, on my pages, +04:49 show a book in this related ratings, +04:51 you would absolutely put the ratings inside the book +04:53 because that means you just do a query against a book +04:55 and you already have that pre-joined data. +04:58 But if it's sort of a data warehouse +05:00 and you're asking all kinds of questions +05:02 from all different sorts of applications, +05:04 then trying to understand, well, what is the right way +05:06 to build my document so it matches the queries +05:08 that I typically do or the ones that I need to be +05:10 really quick and fast? +05:12 That becomes hard because there's all +05:13 these different queries and +05:15 the way you model for one is actually the opposite +05:18 of the way you model for the other. +05:20 So depending on how focused your application is, +05:22 or how many applications are using the database, +05:24 you'll have a different answer to this question. +05:26 So the more specific your queries are, +05:28 the more likely you are to embed things and structure them +05:31 exactly to match those queries. +05:33 Related to that, is are you working with an +05:36 integration database or an application database? +05:39 We'll get to that next. diff --git a/video_transcripts/3-modeling-documents/3-integration-vs-app-dbs_transcript_final.txt b/video_transcripts/3-modeling-documents/3-integration-vs-app-dbs_transcript_final.txt new file mode 100755 index 0000000..6b079c8 --- /dev/null +++ b/video_transcripts/3-modeling-documents/3-integration-vs-app-dbs_transcript_final.txt @@ -0,0 +1,62 @@ +00:00 So wait, what is an integration database? +00:02 If you were just working on your own personal website +00:05 or some small project, +00:06 you don't have an integration database. +00:08 But if you work in a big enterprise, +00:10 a big corporation where there's many internal systems, +00:13 you may be working with an integration database. +00:15 Honestly, that's not a great fit +00:17 for NoSQL databases in general. +00:19 It also makes designing documents +00:21 for them more difficult. +00:22 In large corporations where you have many applications +00:26 that share the same data, +00:27 one way that we have built applications to share data +00:31 is to just share the same database. +00:33 We might have a bunch of different applications +00:36 and they're all going to talk to the same database +00:38 so they all have the same concept of a user, +00:40 they all have the same concept of an order, +00:42 things like that. +00:43 And this means the concept of the user +00:45 is as complicated as it can get. +00:47 Maybe the application on the top left +00:48 could have a real simple user, +00:49 the one on the top right actually needs something else, +00:52 the bottom one is something else still +00:54 until you've got a model across all these applications, +00:56 and that makes it super tricky. +00:58 Also in NoSQL databases and document databases, +01:01 the relationships are enforced in the application, +01:03 so that means all of these have to agree on +01:05 what the constraints are, what the relationships are, +01:08 and that can actually cause data integrity issues. +01:10 There's a lot of reasons +01:11 that an integration database isn't a great idea +01:13 for relational databases. +01:14 In fact, it's not a great idea at all, but it has been used +01:17 and because there's different applications +01:19 with different query patterns, +01:20 it makes designing your documents more difficult. +01:23 So instead what do we do? +01:24 We build application databases. +01:26 Maybe we have a bunch of different applications +01:28 just like before, but they all have their own data store +01:30 and they all talk to their own databases. +01:33 Of course they need to exchange data before +01:35 so maybe we do some sort of microservice thing +01:37 where they talk to a service bus +01:38 or they just talk to each other, things like that. +01:40 That means each individual database +01:43 and interaction with its own application +01:45 is super, super focused and limited. +01:48 Here in these cases, +01:49 MongoDB document databases make a lot more sense, +01:52 and it's easier to design the documents +01:54 because the range of queries is extremely focused +01:58 so you can target those particular questions +02:00 against a small set of queries, +02:03 the guidelines we just talked about. +02:04 So this is the kind of model you want to have +02:06 if you're doing data exchange within your organization, +02:10 and you're working with a document database. diff --git a/video_transcripts/3-modeling-documents/4-getting-demo-starter-code_transcript_final.txt b/video_transcripts/3-modeling-documents/4-getting-demo-starter-code_transcript_final.txt new file mode 100755 index 0000000..b054736 --- /dev/null +++ b/video_transcripts/3-modeling-documents/4-getting-demo-starter-code_transcript_final.txt @@ -0,0 +1,157 @@ +00:00 I don't know about you, +00:01 but I feel like we've talked about coding +00:03 and talked about MongoDB in theory enough. +00:05 And it's time to write some code, and use MongoDB. +00:08 So that brings us to getting started +00:09 with our demo application. +00:11 Throughout the rest of the course, +00:12 we're going to spend a significant amount +00:13 of time focusing on this. +00:16 And remember, we're building Snakebnb. +00:18 This wonderful experience where snake owners +00:21 and their pets can share other snake cages +00:24 when they're traveling. +00:25 So they feel totally comfortable +00:26 on every vacation you need to take your snake on. +00:28 Of course, it's just a knockoff of Airbnb type thing. +00:30 And in this video, +00:32 we're going to see how to get it from GitHub +00:34 and how to get it up and running in Python and PyCharm. +00:36 So we'll start out over here on +00:38 github.com/mikeckennedy/mongodb-quickstart-course. +00:42 And you can see that we've got a couple things here. +00:45 We've got some data. +00:46 This is empty right now, but I'm going to fill it up +00:47 with stuff as we go through the class. +00:49 So you'll be able to recreate the database. +00:51 There'll be instructions in there on how to restore that. +00:53 Then if you go over to source, +00:54 this is the most interesting part. +00:56 We're going to be working in this area here, +00:59 but I've made a snapshot of starter code Snakebnb. +01:02 And this is exactly a snapshot of what we're starting from. +01:05 Okay, but I'm going to be working in here +01:07 because I want to have it build up, right? +01:09 Also, try to do, make some branches or other save points, +01:12 really obvious when we get to the various videos. +01:15 Right now there's no other branches, but we'll get to those. +01:17 Okay, let's go and check this out. +01:19 So we'll go copy what we need. +01:21 And we'll say "git clone" this. +01:24 Nice and quick, and let's go work with it. +01:26 Over here, +01:29 we have our source code, and we have our Snakebnb, +01:32 and we have our starter code Snakebnb. +01:34 So these are the two projects here. +01:37 And what I want to do is I'm going to put this into PyCharm. +01:40 On macOS you can just drag and drop this onto PyCharm, +01:43 and it'll load the project from that folder. +01:45 However, if you do this on Windows or on Linux, +01:49 I think you have to go to PyCharm and say, +01:50 File, Open Directory. +01:52 However, before I do, let's go into this folder really quick +01:56 and create a virtual environment. +01:57 So you may be familiar with Python, +01:58 and virtual environments, and so on, +02:00 but if you're not, let me give you the quick overview +02:02 of what's going on here. +02:04 If we look here we're going to have, +02:06 apparently, a misspelled requirements file, +02:09 which we're going to take care of in a second. +02:10 But notice in this requirements file, +02:12 these are the external libraries. +02:13 PyMongo and MongoEngine for MongoDB +02:16 and some other random stuff for working with color +02:20 output on the console, +02:21 as well as parsing date times entered from the user. +02:25 So we need these libraries, +02:26 and we don't want to install them and manage them, +02:28 basically, as a machine-wide thing. +02:30 We want to install them into our virtual environment. +02:33 So, let's go over here first. +02:35 Your name requirements. +02:37 And we're going to go and actually +02:39 create the virtual environment, and then we'll install. +02:42 We can install stuff into it. +02:43 So here we are again in this source folder. +02:45 So we'll say Python3-M VENV. +02:49 So run the virtual environment module into .env. +02:52 This naming convention, .env, +02:54 is something that PyCharm understands, +02:56 will automatically detect, and start using. +02:58 We're going to pass a test copies flag. +03:00 That's only required on macOS, I believe. +03:02 But, anyway, we'll go with that. +03:03 Now, if we do an LSAH, you can see this hidden .env. +03:07 But we don't need to do anything else with it, +03:09 PyCharm should take it from here. +03:10 So we can go and grab this folder. +03:12 On macOS, remember, File, Open Directory on the other OSs, +03:15 and drop it here. +03:17 Let's go ahead and tell PyCharm about git. +03:20 For the very first time PyCharm will index +03:22 the Python environment we gave it. +03:25 Then it should be up and running. +03:26 Okay, so let's look dow here, the terminal. +03:28 You should see the .env +03:30 You can ask questions like, which Python? +03:32 And it shows you it's the one that we created. +03:35 In Window's it's "where Python," not "which Python." +03:38 If we go over here we have our requirements and so on. +03:40 Now, the other thing we need to do +03:42 is we need to right click and say, +03:43 set this as the relative path. +03:46 In this file, when I import some other file, +03:50 it looks relative to that. +03:51 You can right click here and say, +03:53 mark directory as sources route, +03:55 or just be in this folder when you run it in Python. +03:58 And basically, your working directory. +03:59 Okay, so we're almost ready to run things. +04:01 The last thing we need to do is install these requirements. +04:03 So we can say, "pip install -r" the requirements file. +04:08 And that will install those libraries for us, +04:10 so when we run the application it has everything it needs. +04:13 So if we try to run it now, it'll crash and say, +04:14 it can't find Colorama or something like that. +04:17 Now, this application is empty. +04:18 It doesn't do anything +04:19 other than ask for a couple of prompts. +04:21 There's no data access for MongoDB, anything in here. +04:24 But let's go ahead and just get it to run. +04:25 So we can right click on "Program," +04:27 and right click and say "Run Program." +04:30 It runs, and you can see, if I make it bigger, +04:33 here we have our Snakebnb, +04:34 and I put a little snake there for your guys. +04:36 And it asks you a question: "Are you a guest or a host?" +04:38 Are you looking for a cage, +04:39 or do you want to offer up your cage? +04:41 So let's go with guest. +04:43 And it lets you do things like create an account, +04:46 add your snake, and so on. +04:47 So I could say, I'd like to log in. +04:49 It says, you know, that's not implemented yet. +04:51 In fact, that's what we're going to be doing next, +04:53 implementing all of these features in the database, +04:56 creating an account, logging in, booking a cage, +04:59 viewing cages, things like that. +05:01 All the actions you might do +05:02 in a typical Airbnb situation. +05:04 So that's it for now. +05:06 We have this up and running. +05:07 Let's do one more thing. +05:09 Because of the output, I find this looks a little better +05:11 if we just run it separate outside of PyCharm. +05:14 So we can say copy the path here. +05:17 And we're still in this folder with the .environment, +05:20 so we need to activate it if we're going to run it over here. +05:22 So we would say ". .env/bin/activate" +05:28 On Windows, you don't need the first dot, +05:30 and it's not "bin," it's "script." +05:31 Script or scripts, I can't remember. +05:33 I think it's "scripts." +05:34 Either way, a prompt should change. +05:36 And now we can run this. +05:39 Here's our snake again. +05:40 Okay, so we're all set up and ready to run our code. diff --git a/video_transcripts/4-mongoengine/1-how-odms-work_transcript_final.txt b/video_transcripts/4-mongoengine/1-how-odms-work_transcript_final.txt new file mode 100755 index 0000000..e73e36d --- /dev/null +++ b/video_transcripts/4-mongoengine/1-how-odms-work_transcript_final.txt @@ -0,0 +1,69 @@ +00:01 It's time to write some code against MongoDB +00:04 and connect to MongoDB and we're going to do that +00:05 with an ODM: an Object Document Mapper. +00:08 If this term's new to you, +00:09 think of Object-Relational Mapper, like SQLAlchemy, +00:12 but for document databases instead. +00:14 So, let's compare first this ODM-style of programming +00:18 against the most basic, lowest level way to program, +00:22 or interact with, MongoDB from Python called PyMongo. +00:25 Every programming language +00:26 that you can talk to MongoDB from, see there is many of them +00:29 20, 30, something like that. +00:31 Many, many languages can talk to MongoDB +00:34 and they each have what's called a driver, +00:35 and this is typically provided by MongoDB, +00:38 the company itself. +00:39 PyMongo is no different. +00:41 It's this low level foundational way to talk to MongoDB +00:46 and you do this in the native query syntax +00:48 of MongoDB, this Java Script JSON-style +00:52 of interacting with the database. +00:53 Now, it's important to know that +00:55 if you're working with MongoDB +00:56 in terms of running and managing it, +00:58 but from writing code, we're going to focus on something +01:01 higher level: an ODM, so we can take structured classes +01:05 and map those to and from the database. +01:07 So let's see how it would work if we just used PyMongo. +01:09 We've got our app here and we have the PyMongo package +01:13 we're going to work with, and we have MongoDB, the database. +01:15 We write direct queries in this raw MongoDB API. +01:20 You have to know the API really carefully. +01:22 You have to map those back to your classes. +01:24 Basically what you do is you pass dictionaries to PyMongo. +01:28 It uses those as part of the query +01:29 and then you get the dictionaries back. +01:31 It's pretty unstructured but it's very low-level and fast. +01:33 With an ODM, similarly, we've got our app +01:36 and we've got PyMongo and MongoDB, +01:38 but we also have another layer, +01:40 the layer that we directly interact with, +01:41 called the ODM, Object Document Mapper. +01:44 And there's a bunch of different kinds. +01:45 There's MongoEngine, there's Ming, there's MongoKit, +01:48 there's MongoAlchemy, MiniMongo, +01:51 and there's more than that actually; there's a ton of them. +01:54 Just so happens we're going to use MongoEngine, +01:55 one of the more popular and well-polished ones. +01:58 So, in this model, we don't query in terms +02:01 of raw dictionaries against PyMongo, +02:02 we just talk to the classes to find by the ODM. +02:05 And our queries are based on those types, on those classes. +02:09 That itself will translate to the MongoDB API +02:13 sometimes in real basic ways, +02:14 sometimes in really advanced ways, +02:15 and it'll actually leverage some of the advanced operators. +02:18 The dollar operators, if you're familiar with them, +02:21 for MongoDB like dollar set, dollar add to set, +02:23 things like this. +02:24 So really, really cool that it leverages +02:27 the advanced operators, not just save this document, +02:30 read this document-type programming. +02:32 I think the ODM model is a much +02:34 better way to write your application. +02:36 You'll see there's not much structure +02:37 in a schema-less database, so having a little bit +02:41 of extra structure to find by these classes +02:43 that are a part of this ODM model really adds +02:46 a lot of safety to your application, in my opinion. \ No newline at end of file diff --git a/video_transcripts/4-mongoengine/2-intro-to-mongoengine_transcript_final.txt b/video_transcripts/4-mongoengine/2-intro-to-mongoengine_transcript_final.txt new file mode 100755 index 0000000..7fcb4a5 --- /dev/null +++ b/video_transcripts/4-mongoengine/2-intro-to-mongoengine_transcript_final.txt @@ -0,0 +1,32 @@ +00:00 The ODM we're going to use for this course +00:01 is MongoEngine, and you can find it's homepage +00:04 and details about it, documentation and so on, +00:07 at MongoEngine.org. +00:08 So MongoEngine is open source like many things +00:11 you'll find in Python, as we said, +00:12 it depends upon PyMongo. +00:14 You saw us install it earlier, +00:16 we just did Pip install MongoEngine +00:18 and that installed PyMongo with it. +00:20 We did that through the requirements file +00:21 but you can do that directly if you prefer. +00:23 You can find MongoEngine on GitHub. +00:26 You can see that it's quite popular, +00:27 almost 2,000 stars. +00:29 This is much more popular that the other MongoDB ODMs, +00:33 as far as I can tell, looking at the other ones, +00:35 this is the definitely most popular, +00:37 at least, among the most popular of them, +00:39 and it's very actively under development. +00:41 I just took this screenshot right now +00:43 before I started recording here +00:45 to give you the latest version. +00:46 You can see that it's been updated in the last 24 hours, +00:50 and some other stuff under the actual code +00:53 has been updated in the last 21 hours, +00:56 so very active, this is important +00:57 for an open source project you're going to depend upon, +01:00 so I think you know, judging by that, +01:02 MongoEngine is the best choice, +01:03 and the API is excellent, +01:04 we're going to start working with it next. diff --git a/video_transcripts/5-building-with-mongoengine/10-demo-register-cage_transcript_final.txt b/video_transcripts/5-building-with-mongoengine/10-demo-register-cage_transcript_final.txt new file mode 100755 index 0000000..0e9bda3 --- /dev/null +++ b/video_transcripts/5-building-with-mongoengine/10-demo-register-cage_transcript_final.txt @@ -0,0 +1,208 @@ +00:00 Next thing we need to do as the host +00:03 is to be able to register our cage so that +00:05 people can view it and maybe book it. +00:07 That'd be great, right? +00:08 So, let's go over here and work with that. +00:11 Now, the cage has to be associated with an owner +00:14 through this software key relationship in MongoDB. +00:18 We're going to require an account, +00:19 and that's going to look like just an error message. +00:25 We'll just say if there's no account, +00:27 you must log in first to register a cage. +00:29 Alright, so now we have the account +00:31 and we're going to use that as part of it. +00:33 Let's go ahead and figure out how many square meters +00:36 this is going to be. +00:38 We'll say "meters = " something like this. +00:47 You might want to just directly convert that into a float. +00:50 But I found as I was interacting with this application +00:52 I'd accidentally go to register a cage +00:55 and I meant to list them. +00:56 You want some kind of way to cancel out, +00:57 so let's just suppose if they don't enter anything, +00:59 they just hit enter, it kind of short circuits everything. +01:02 We'll say "if not" something like this, +01:07 so if they don't enter anything, we'll just bail out. +01:09 Otherwise we'll just say, float of meters, +01:13 so convert it from a string to a float. +01:15 We're not doing the error handling on this. +01:17 You guys should probably add that, but we can just do this. +01:20 And we have to ask a bunch of other questions. +01:22 Is it carpeted, does it have toys? +01:24 Thing like that. +01:25 Let me just copy that over . +01:29 This is just user input stuff, right? +01:32 And then, we want to go down +01:34 and we want to actually register the cage. +01:37 Again, we want to do this at our data layer, +01:39 not here in our application code, +01:41 so we'll say "svc.register_cage()" and again, +01:44 that doesn't exist, but it's going to in a second. +01:47 We are going to pass the account. +01:52 We'll pass the active account here, +01:54 we'll pass the name of the cage, +01:57 we'll pass whether or not it allows dangerous, +02:00 just all of these items. +02:02 Whether or not it has toys, +02:04 whether it's carpeted, and the meters. +02:10 Okay, so we're going to go and call this function which, +02:13 obviously, doesn't exist yet, +02:15 but PyCharm will write it for us. +02:17 Thank you, PyCharm. +02:19 Here we can say this is an owner, +02:21 here's the name of the string, and so on. +02:24 There we go. +02:25 Let's say this is going to return a cage +02:28 which we have to import at the top. +02:29 Again, thank you PyCharm. +02:31 This is very, very similar to the create account. +02:33 We're just going to create a cage and save it. +02:39 We've set all the properties and we'll call "cage.save()" +02:42 and that's going to store it in the database. +02:45 Now, we want to, remember if we look over at our owners, +02:49 it has cage IDs to manage the relationships. +02:53 The order in which we do this is super important. +02:57 Now, we need to be a little bit careful +03:00 with this account here. +03:02 We want to make sure that we're getting the latest +03:05 account from the database, so we'll do something like this. +03:08 We'll say "account = find_account_by_email" +03:14 this "active_account.email" +03:17 That makes sure we don't have any stale data. +03:19 Then we're going to go "account.cage_ids" +03:24 This is a list, so we can append to the cage.ID. +03:28 It's super important, the order here. +03:31 We must call save so that this is an actual generated value. +03:36 It's just none beforehand, so we want to make sure +03:38 that's generated in the database +03:40 and then we can return the cage. +03:42 So, that warning up there goes away +03:45 because we are returning the cage. +03:49 The other thing that we need to do is, +03:52 this has changed the account, +03:54 but we haven't pushed those changes back to the database. +04:00 Alright, so our registered cage seems like it's working. +04:03 Let's go over here, and let's do one more thing. +04:06 This active account needs to have that data adjusted as well +04:11 so let's go over here to state, and has a reload account. +04:15 That's not written yet, let's do that. +04:18 This is super easy to do. +04:19 All we have to do is go to the database and pull it back +04:23 so we have this active account global variable +04:25 and we can come over here and say import the service +04:30 and here we're going to say "find_account_by_email" +04:32 "(active_account.email)", alright? +04:35 That's all we've got to do to reload it. +04:37 So, this'll make sure that it can work +04:40 with our "state.active_account" +04:41 It's the fresh one that just got its data changed down here. +04:46 Let's do one more thing before we carry on. +04:48 Let's go ahead and implement register cage here, +04:52 and let's spell suppress correctly. +04:54 Let's add the ability to list our cages +04:57 so that we can test that this actually worked. +04:59 Let's also do a success message, +05:02 "Registered new cage with id" +05:09 Make that an F string, actually. +05:12 "cage.ID" so we've got to store that up here. +05:19 Excellent, so now we've got our cage +05:20 and we'll see that come out, but let's go ahead +05:22 and we're going to require an account again, +05:24 which is the same info up here. +05:31 And all we've got to do is get the cages. +05:33 So, let's write like this. +05:36 Let's say, "cages = svc.find_cages_for_user" +05:44 Right, got to write that function. +05:46 There we go, creates an account. +05:49 Let's not call it active account, let's just say account +05:52 and that is an owner, and it returns a cage. +05:58 Actually, it's not a cage, what does it return? +05:59 It returns a list of cages, so we've got to go +06:01 to the typing module here, typing, and new cage. +06:06 Okay, perfect. +06:07 What we're going to do is, we already have the owner, +06:10 and because of our refresh account stuff, it should be fresh +06:14 so we'll have cage IDs, and we should have this +06:17 "account.cage_ids" right there that we can use. +06:21 Now, how do we query for this particular thing? +06:25 We're going to do something you haven't seen yet. +06:27 We're going to come to the cage, and again, +06:29 we're going to go to this objects and I'm going to do the query, +06:31 but instead of saying, remember before we had "email = " +06:34 and that did the query against MongoDB for testing quality? +06:38 We have something different we need to do. +06:40 We want to go to the ID of the cage, +06:42 and we don't want to just say, well, it's equal to, +06:45 it's not going to be equal to the list. +06:47 We can't say not cage IDs, because one is an object ID +06:51 and one is a list of object IDs, +06:54 so we have to use a special operator, +06:56 and MongoDB has all these simple dollar operators. +07:00 $set, $in, $not, $or, +07:04 these types of things. +07:07 And the way that we work with those in MongoEngine +07:10 is we use a double underscore to say +07:12 we're applying this to the ID but then there's this other +07:15 thing that we're doing and we're going to say "in" +07:18 This query right here, do a little cleanup, +07:21 this query says go to the cage and find all the cages +07:24 whose ID is in this list of IDs. +07:26 Now, we'll have "cages = " this is a query +07:32 but we want to execute the query and sort of snapshot it +07:35 for our app so it'll return cages. +07:41 And that should make our app totally happy. +07:44 That little warning, up and away. +07:46 Okay, great, so we've written this function +07:48 and we've used the "in" operator, +07:50 the double underscore, to access it. +07:52 Here's our cages, we just need to print them out. +07:54 Or see in cages... +07:58 Let's make these F strings, and we'll put them here +08:00 and we'll say "c." whatever we want here. +08:02 We want name... +08:06 Let's also print out something like this. +08:10 You have however many lengths of cages you have +08:13 and then we'll print those off, okay? +08:15 Let's try to test these two things that we've written. +08:19 For host, we first want to register a cage. +08:22 Actually, let's try to list our cages. +08:30 Login "michael@talkpython.fm." Let's list our cages +08:32 You have zero cages, great, so let's register a cage. +08:37 Now we're logged in it'll let us. +08:39 It's 2.2 square meters. +08:41 Yes, it's carpeted. +08:42 Yes, it has toys. +08:43 No, it has no venomous. +08:45 This'll be Bully's Cage. +08:49 It looks like we haven't set the price, hmm. +08:52 We've forgotten something, haven't we? +08:53 Okay, good thing we tested that here. +08:59 The price, and this will be, how much are you charging? +09:03 Alright, let's go ahead. +09:04 Price here, price, okay. +09:09 That was kind of annoying that that crashed, +09:12 but it's also cool. +09:13 Why is that cool? +09:14 Because if we said the cage must have a price and it didn't, +09:17 if that was regular MongoDB, that would've just let +09:19 that happen, but because it was MongoEngine, it did not. +09:22 Alright, let's do this again. +09:31 List my cages. +09:33 Whoops, list my cages. +09:34 We have no cages, let's register a cage and do this again. +09:42 How much are we charging? +09:44 We're charging $29 a night. +09:46 This is one fancy cage, folks. +09:48 Boom, we've registered a new cage. +09:50 Now, let's list your cages. +09:52 Ooh, we have "1 cages." +09:54 Maybe a plurality thing there, but Bully's cage is out. +09:57 Let's register one more cage. +10:00 This is a huge cage. +10:02 It's carpeted, has all the toys, +10:03 and this one's even for venomous snakes. +10:05 This would be the "Large boa cage," who knows. +10:11 And this is $39. +10:12 Now if we look at our cages, we have two cages. +10:16 Beautiful, so it looks like our registering +10:18 and our listing cages is working great. diff --git a/video_transcripts/5-building-with-mongoengine/11-demo-add-a-bookable-time_transcript_final.txt b/video_transcripts/5-building-with-mongoengine/11-demo-add-a-bookable-time_transcript_final.txt new file mode 100755 index 0000000..f2b50c9 --- /dev/null +++ b/video_transcripts/5-building-with-mongoengine/11-demo-add-a-bookable-time_transcript_final.txt @@ -0,0 +1,121 @@ +00:01 Let's add a bookable time to one of our existing cages. +00:05 We were able to register cages and list them. +00:07 Now let's make them available for snakes to stay in. +00:12 We're going to work on this update availability +00:14 that we did here. +00:15 We've got our requirement that you have to have an account +00:18 because they're your cages, +00:20 whose cages you're going to manage, things like that. +00:23 And we're going to just print out the list cages. +00:25 I've changed that slightly so it has a number. +00:27 I'll show you really quick. +00:29 I'm using enumerate in an index. +00:32 It says one, two, three instead of star, star, star +00:34 because we want to ask like, +00:35 "hey, what cage do you want to work with?" +00:38 We're going to add that here +00:40 and just for the sake of time I'm just going to paste that. +00:42 So it's going to say, "What cage do you want?" +00:45 Either you can cancel or it's going to parse that +00:47 into an integer +00:48 and then we're going to work with a particular cage. +00:51 Let's go down here and say "selected_cage" +00:55 Oh, first we need the cages. +00:56 Let's say "cages = " we'll just use our service again. +01:00 Get the cages for "state.active_account" +01:05 We'll go down here and say "cages[cage_number-1] +01:10 Because we're showing it to them one, two, three. +01:11 We got to convert that back to zero base. +01:15 Once we've gotten our cage set, we need to get the dates. +01:18 We've selected this cage. +01:19 We'll say the name that you're going to work with. +01:20 We're going to say enter date, year, month, day +01:24 that you'd like to start this available time slot on +01:27 and for how many days. +01:28 I want to start a particular date +01:30 and make that available for five days. +01:34 We're using this thing called parser. +01:37 That comes from "dateutil" +01:39 so python-dateutil is the module name. +01:41 It's in the requirements already. +01:43 Parser is a really sweet way to parse date times. +01:47 It has many, many different formats it understands. +01:50 We're going to use that +01:51 instead of the built-in date-time parsing. +01:53 Once we have this, we just need to go and use our service +01:56 and write another data access method. +01:58 We'll say "svc.add_available_date" +02:02 What are we going to pass? +02:02 We have to have the account. +02:03 Let's say active account there. +02:07 Select the cage that we're going to add it to. +02:09 We have to have the start date and the days. +02:13 Let's go ahead and say "state.reload_account" +02:16 This might change the account. +02:19 Then we want to have a little success message like +02:22 hey, good job. +02:26 And F String this. +02:32 Now we're down to just pure data access and MOGO, +02:35 the interesting part, right? +02:37 Let's go have PyCharm add that function. +02:41 We'll have a few things here. +02:43 This is going to be an owner. +02:48 Cage, date, time. +02:58 And an "int" and it's going to return nothing. +03:01 This looks great. +03:02 Remember what we're adding here is actually a booking. +03:06 Bookings are not top-level items. +03:08 But we'll go and create one to get started. +03:10 I'm going to say booking. +03:11 It's a booking like so. +03:14 We just got to set the properties. +03:17 This one we got to do a tiny bit of math here. +03:20 We'll say "start_date + timedelta" +03:30 Now we want to change the cage. +03:32 The way it's working probably is fine. +03:34 Just change the cage and call save. +03:36 But I want to make sure that we absolutely are working +03:39 with what's in the database. +03:40 So I'll say "cage = " +03:44 Actually we can just look it up here, I think. +03:46 I'm not sure if we need it again. +03:48 We'll just say "cage.objects(id=cage.id).first()" +03:55 Then we're going to go over here, select the cage that was. +03:59 Let's check that here. +04:03 Again remember the bookings are not top-level items. +04:05 They live inside of the cages. +04:07 Here we're going to append the booking here +04:09 and we call save on the cage not on the booking object. +04:15 It doesn't live on its own, it lives inside the cages. +04:20 Here we can return a cage, I suppose, if we want. +04:23 Here's kind of the updated cage. +04:25 We could even tell consumers that that happens. +04:30 I guess we don't need our active account here, do we? +04:32 So we can go ahead and drop that. +04:34 Let's just do a quick clean-up here, get rid of this. +04:37 We're not doing that, nothing changes there. +04:39 Okay, great. +04:40 It looks like we can probably add some time. +04:43 Let's go and try to test this out here. +04:46 Going to be the host, we need a log in. +04:49 Let's see our cages. +04:51 We've got these two, let's update the cage availability. +04:54 The large boa constrictor one is available +04:57 so that's going to be number two. +04:58 Great, we've selected it. +05:00 This is going to be 2018/01/01. +05:06 Brand new year, cages available. +05:09 Let's say that's for five days. +05:11 Great, a date was added. +05:12 Let's add one more. +05:16 Cool, now if we list our cages, +05:18 you can see our low large boa constrictor cage +05:21 now has two available bookings. +05:23 It has this time for five days and that time for ten days, +05:26 And neither of them are booked +05:28 because no guests have come along and actually booked it. +05:30 But it's available and now they can go and ask, +05:34 hey what cages are available for my snake? +05:37 When they ask, this large boa cage should come up. +05:40 Maybe we'll make a little side money +05:42 while our boa constrictor is not using it. diff --git a/video_transcripts/5-building-with-mongoengine/12-demo-managing-snakes_transcript_final.txt b/video_transcripts/5-building-with-mongoengine/12-demo-managing-snakes_transcript_final.txt new file mode 100755 index 0000000..00fc352 --- /dev/null +++ b/video_transcripts/5-building-with-mongoengine/12-demo-managing-snakes_transcript_final.txt @@ -0,0 +1,86 @@ +00:01 So before we finish the host side, +00:03 where you actually can see your bookings +00:05 and things like that, +00:06 that turns out to be one of the most complex +00:08 types of queries we're doing in this entire application. +00:12 I want to make sure that you can book some stuff, +00:14 you can see it so the data comes out when we write it. +00:16 That'll make a lot more sense. +00:17 So let's take a moment and focus +00:18 on the guest side of the things. +00:21 Let people come in, log in, create an account, +00:24 register their snakes and so on. +00:26 So if we go up to the top +00:27 to our little switch action thing here, +00:30 notice that I'm using the create and login from host, +00:33 there's no reason to write that code twice, +00:35 we'll just use that one. +00:36 Now the thing I want to focus on for a moment is +00:38 adding a snake and viewing your snake. +00:40 Now this is super, super similar +00:42 to what we already did so let's go ahead +00:44 and just talk through this real quick +00:46 instead of write it from scratch. +00:47 So just like before, we have to have an account, +00:50 ask a few basic questions like what is your snake's name, +00:53 let them cancel by hitting nothing in the first one, +00:55 get the information about the snake +00:57 and then we call the function "add snake" +00:59 and you know, you could see this is very, very similar +01:02 so we're going to get the owner, +01:03 get the owner back and maybe it makes more sense +01:06 because we've been writing others like this, +01:07 to code like this. +01:09 We're going to create the snake and save it, +01:10 but remember the relationship between snake and owners +01:13 is managed by the snake IDs inside of the owners, +01:17 so we're going to go get a fresh copy of the owner +01:18 from the database, +01:20 update that, and save it. +01:22 Then we'll go back to the snake that we just created. +01:25 Also, we'll call a quick reload account +01:27 to make sure the snake ID, +01:28 for some reason if they get reused real quick, +01:29 are in the active in memory account. +01:32 So that's all it means to create a snake, +01:35 just like creating a cage was. +01:38 So everything's exactly the same there. +01:40 And then to view your snakes, +01:42 we just write the function, +01:43 get me the snakes for the user, +01:45 and we loop over them. +01:47 That again, is very much like the cages, +01:50 we get the owner, in this case just to be sure +01:53 that we have the fresh set of IDs there, +01:56 and then we do the ID in "owner.snake_ids" +01:59 and we can call to all function, +02:00 and then convert it to a list. +02:01 I suppose we could probably skip this, +02:03 but either way, this is all good. +02:05 So this gives us our snakes, +02:07 and we're going to list it out. +02:08 Let's go ahead and just run that to make sure +02:10 this is all working here. +02:11 This time, we're going to be a guest. +02:13 And let's go ahead and log in +02:14 and this time I want to log in as Sarah. +02:16 So Sarah's going to be my guest, +02:18 and Michael is going to be the person with the cages. +02:21 Login, so we're logging in as Sarah. +02:24 And let's say I'd like to view my snakes. +02:27 Hmm, you have zero snakes. +02:29 Okay, let's add a snake. +02:31 They'll have Slither, +02:33 and Slither is 1.2 meters long, +02:36 this is a large gardener, +02:39 and no those are not venomous, +02:40 so we've created Slither. +02:42 And let's add one more snake. +02:43 This is going to be Bully, +02:46 and Bully is .5, .4 meters let's say, +02:49 this is a bull snake. +02:52 Now if we say view your snakes, +02:55 there's your two snakes. +02:56 Okay, so our guest side of registering a snake +02:59 so that we can book it into a cage, +03:01 and viewing it, that kind of stuff, is all finished. diff --git a/video_transcripts/5-building-with-mongoengine/13-demo-book-a-cage_transcript_final.txt b/video_transcripts/5-building-with-mongoengine/13-demo-book-a-cage_transcript_final.txt new file mode 100755 index 0000000..8659090 --- /dev/null +++ b/video_transcripts/5-building-with-mongoengine/13-demo-book-a-cage_transcript_final.txt @@ -0,0 +1,338 @@ +00:00 So, we have cages. +00:02 We have available dates in cages, and we have snakes. +00:05 Time to book a snake into a cage +00:07 on one of those available dates. +00:08 This turns out to be one of the more complicated things +00:10 we're going to do in this application. +00:12 And there's a lot of input, and juggling, and stuff. +00:15 So I'm going to paste a few things here +00:17 just about asking questions about the dates and stuff. +00:21 And then we're going to go and write +00:22 the database queries from scratch. +00:25 So let's start here. +00:26 We're going to start by making sure you have an account. +00:29 And then we're going to get all the snakes +00:31 and make sure you have a snake, +00:32 because having an account is not enough. +00:34 You also have to have a snake you can put into there. +00:36 We're going to ask some questions +00:38 about when do you want to do this. +00:39 We're going to use Python "dateutil" to parse that. +00:43 So just like before, we're going to do a little error handling +00:45 to make sure you're not trying to book some sort of +00:48 reverse thing like I check out before I check in, +00:53 something like this, right? +00:54 Okay, so then the next thing we need to do +00:56 is find out the available cages. +00:58 And this is where it gets interesting. +01:00 So we're going to write a function called get available cages. +01:02 We're going to take the check in, the check out, and the snake. +01:06 We also need to figure out which snake you want. +01:09 So first of all, let's do an enumerate, +01:11 lift out your snakes, +01:13 and we'll say snake one this, snake two is not, +01:16 and you'll pick the snake, okay. +01:18 So, take our snakes, pick that, got our time +01:22 and then we're going to go to the database +01:23 and find particular cage that we can work with. +01:28 Now, that's not all there is to it. +01:29 So this is just going to get us the cages +01:31 that could be booked, and then we have to ask, +01:34 that's this little section right here, +01:35 and we have to let the user pick a cage, +01:37 and we'll find the underlying booking behind it. +01:40 So let's write this function. +01:41 So this can be a date-time and a snake. +01:51 You, of course, don't have to put the tie pins. +01:53 But I find at the data access layer, +01:56 it's really helpful, maybe through, +01:58 you can see it through the rest of the application, +01:59 we're not doing this. +02:00 But at the data access layer, +02:02 I find it really helpful to say +02:03 these are the things that go in, +02:04 these are the things that go out. +02:05 This is how we're working with the database. +02:08 Okay, so here's where we get down to business. +02:11 We're going to come in here. Let me move this up for you. +02:13 So we need to do a couple of things. +02:15 We need to find all the cages that have bookings, +02:19 that are not booked between this time and that time. +02:23 All right? +02:24 And we need the snake information, +02:25 because not all cages allow poisonous snakes, +02:29 and they don't all necessarily fit. +02:31 If I have a 20 foot snake, I can't put into a two foot cage. +02:35 So let's just do a little quick rule of thumb to say, +02:40 if your snake is four times longer or more than the cage, +02:45 then the snake can't go into it, right? +02:46 Snakes can curl up, but they can only curl up so much. +02:48 So we'll say something like, the minimum size of the cage +02:51 that we're going to get is "snake.length / 4" +02:55 This is going to be part of our query. +02:58 The date's going to be part of the query, +02:59 and whether it's venomous or not. +03:02 We're going to do a few interesting things here. +03:03 This is definitely part of the more complicated queries. +03:06 So I'm going to go to the cage, and we'll say objects. +03:08 And when you have these compact queries, +03:11 I find it's nice to spread this across multiple lines. +03:13 Well, I'll say ".filter" +03:15 And on multiple filters the are effectively and +03:18 I'll say square meters. +03:21 Now, I'd like to say, let's say +03:23 "=min_size" or greater, right? +03:25 Just like we saw with the operators about in, +03:28 there's one for greater than or equal. +03:30 And we can say, the square meters +03:31 are greater than or equal to this minimum size. +03:34 But that's not the only thing that we need. +03:36 We also need to go and do another pretty wild thing. +03:39 We want to go to the bookings. +03:41 Now, remember, refresh, over here we have a cage. +03:44 The cage has a bookings field. +03:46 We go to the definition for bookings. +03:48 Bookings have a check-in date and a check-out date. +03:51 We want to work with that. +03:53 How do we do that in MongoEngine? +03:56 We come over here and we can traverse +03:57 that hierarchy with underscores as well. +03:59 So we can say bookings.check_in_date, +04:02 and we want to have the check in date +04:06 before or equal to the check in that those passed, right? +04:10 So the time you can check in has to precede +04:13 the time this person is checking in. +04:16 And then we'll do something similar for check-out. +04:22 Okay, so this is part of the query. +04:24 Now, if the snake is poisonous, +04:27 we also want to say that they allow poisonous snakes. +04:30 So we'll say this, we'll say if "snake.is_venomous" +04:33 we need to augment this query. +04:35 So we can do that because it hasn't executed yet. +04:38 It's like the potential to be executed. +04:40 So we can say "query = query.filter" +04:43 and is thing "allow_dangerous_snakes" +04:47 That's what we want to work with. +04:51 "=True" +04:52 Because non-dangerous ones can stay +04:54 in cages that will either allow +04:56 or not allow dangerous snakes. +04:57 But if it's venomous, we have to +04:59 have this additional criteria. +05:02 And maybe we want to have some kind of order by, +05:04 like we'd like to show them the cheaper ones. +05:06 So let's go like this. +05:07 We'll say "cages = " +05:09 and we'll finalize the query like this. +05:11 We'll say "query.order_by" +05:16 Now, you don't do this sort of default +05:19 of this named parameter type thing for this. +05:22 We want to order by price. +05:24 And default is ascending, so cheapest ones first, +05:27 and maybe you want to see the biggest ones first as well. +05:29 So we'll say square meters, like this. +05:33 So we're going to say, first order by price, lowest to highest, +05:37 and then show us, if the price is the same, +05:40 show us the largest ones at that price level +05:44 down to the smallest ones. +05:46 Excellent. +05:47 So this is pretty much working. +05:50 It turns out it looks like it's going to completely work, +05:53 but it turns out that there's a challenge +05:55 we're going to run into. +05:56 And in PyMongo this is straightforward to solve, +05:59 although, you have to use a lot of operators, +06:00 these dollar operators to make it work. +06:02 But I haven't found a good way in MongoEngine. +06:04 And so I still find the on balance +06:06 that work with MongoEngine, even for this query, is better. +06:09 But here's the problem. +06:10 What this query is asking. +06:11 You're probably thinking it looks right. +06:14 It takes a moment to realize +06:16 the challenge we're hitting here. +06:18 What this query says is, go to the cage, +06:21 and find me the cage where the square meter is +06:22 at least minimum size. +06:24 That's totally fine, that works perfectly. +06:26 And it says, show me where there's a booking +06:29 query within check out. +06:30 And there's a booking, oops, this should be greater than. +06:36 That was almost an error. +06:37 So where there's a check-out date passed, +06:41 equal to or passed where I'm willing +06:42 to check-out for my snake. +06:45 The problem is if I have, let's say, +06:47 20 bookings in this cage, right, +06:51 I probably want to check one more thing, +06:54 but I can just check it. +06:55 Damn, we're going to have to do one more bit at the bottom. +06:57 But the problem is, what if, there's two bookings? +07:03 One that starts way in the past, +07:05 but the check-out is one day later. +07:08 And then there's another one where +07:10 the check-out date is way in the future, +07:12 but you can only check-in one day before. +07:14 And these are not the same bookings, right? +07:16 There's a booking where the check-in date +07:17 is before the check-in, +07:18 and there's a booking where the check-out date +07:20 is after the check-out, +07:21 but those are not the same. +07:23 You need to say, there's an individual booking, +07:26 not like some set of bookings where one matches one clause, +07:31 and the other matches the other. +07:32 So the way you do that in Mongo, +07:35 is you say, dollar element match. +07:37 I think it's "$elemmatch" +07:39 So element match is the description of the thing. +07:43 So you can say, it must have both of these. +07:46 But I don't see how to do that in MongoEngine. +07:48 It seems like it should be possible, +07:49 it certainly is possible for equality. +07:53 But for these operators plus element match, +07:56 didn't seem to work for me. +07:57 Anyway, you figure it out, feel free to use element match. +08:00 I didn't, so I've got to add one more line here. +08:04 And I'm just going to copy that over real quick, +08:06 and we'll talk about it. +08:07 So what we're going to do is we're going to say, +08:09 let's go and actually, these are the cages we care about. +08:13 I'm going to iterate over the query, which executes it here. +08:16 And remember, the cage, +08:18 each cage contains a number of bookings. +08:19 For each booking, I want to check that both +08:23 the check-in is before and the check-in is after, +08:26 and that the snake ID is none. +08:29 So it's not already booked during that time. +08:32 Though if it's both available, +08:34 and the check-in check-out date matches, +08:38 then we can make that part of our final cage there, +08:41 the final cage list. +08:42 Okay, so, and it says it returns this, +08:45 but actually, what it returns is a list of cages. +08:49 There we go. +08:50 Okay, so that's what we got to do. +08:52 If I could get element match to work with +08:54 greater than less than in MongoEngine, +08:56 this would not be necessary. +08:58 You could just straight up run that query. +09:01 But anyway, it's not a huge deal. +09:03 Remember, this set is already filtered down to where, +09:06 significantly, right, where the check-in and check-outs +09:09 do match, it just happens to be +09:11 maybe one more thing is missing there. +09:13 Okay, so we're getting available dates. +09:15 So let's come back to our guest here. +09:17 We've got our available cages. +09:19 Now, we just have to like show them to the user, +09:22 and let them pick it. +09:25 All right, so this just takes some +09:26 pre-written code for this. +09:27 There are a certain number of cages available, +09:29 and we're going to enumerate over them. +09:31 And don't need the average radian right now. +09:42 Do it like this. +09:43 We're just going to print out the name, the square meters, +09:44 whether it's carpeted, and whether or not it has toys. +09:47 We want that to be true-false. +09:48 So let's put that "yes and no," more friendly, right? +09:51 And if there's no cages, "sorry, there's no cages." +09:54 But if there are, we'll ask you which one, +09:56 and we'll pick that out in a zero based, of course. +10:00 And finally, the final thing to do is going to be book a cage. +10:03 And then actually, we'll just give out +10:05 this nice little success message saying, +10:08 "Hey, you booked it for this time." +10:10 So last thing to do with this booking a cage +10:12 is to actually book it. +10:17 So let's go over here. +10:18 Put my term, write it one more time here. +10:21 And now, what we're going to do is +10:23 we're going to loop over that cages booking. +10:27 So the way it works is, they've selected a cage. +10:31 They haven't selected the individual booking. +10:33 So we just have to go one more time over the bookings, +10:35 and go, let's find a booking within this cage, +10:38 which we know exists because it's in the list, +10:40 and let's assign it to the snake. +10:43 So we'll do something like this. +10:44 We'll come down here, and we'll start up by +10:46 sending this little booking to nothing +10:49 just in case for some odd reason we don't find one. +10:52 I'm going to go through and, again, +10:53 do a similar test as we did right there, right? +10:57 We got to find the available booking within the cage. +10:59 We know it exists, but we got to find it. +11:01 Then down here we're going to just set a few things. +11:04 Say the "booking.guest" +11:08 and get a little telesense auto-completion if you want. +11:14 Set the guest owner ID. +11:18 I guess we probably got to pass the account as well. +11:23 We'll say "account.ID" +11:27 Say booking dot +11:34 Set the booked date. +11:36 It was booked right now, regardless of when the booking was. +11:40 Then we also need to set the snake. +11:47 There we go. +11:48 And then we got to go back and save it, +11:51 but remember, we don't call save there. +11:53 We call a "cage.save()" +11:55 Okay, excellent. +11:56 Now, I think it just believes that's a misspelling, +11:59 but I'm going to say it's not. +12:01 All right, so that should let us book a cage. +12:04 That was a tough one, right? +12:05 So pretty interesting query. +12:08 We're using the operators, greater than less than. +12:11 We're traversing the hierarchy. +12:13 And like I said, that we're sort of effectively +12:16 in memory applying this element match. +12:18 Element match works in MongoEngine, +12:20 but I couldn't get it to work with both +12:22 element match and the operators. +12:24 So anyway, this will be fine. +12:27 Come down here and given a cage, we'll pull out the booking. +12:31 We probably could structure it slightly differently, +12:33 so we could skip this step and somehow +12:35 capture the booking directly, but this is fine. +12:37 It works plenty fast forwarded. +12:39 Set the, hey you booked it, values of the booking, +12:43 and call save. +12:44 All right, I think it's time for us to test our book a cage. +12:49 And I notice I almost forgot to add this here, +12:52 "state.active_account" I added it below. +12:55 So let's go ahead and run this. +12:57 And we'll come in here, and we'll be, oops, be a guest. +13:01 And let's go ahead and log in. +13:06 And let's see our snakes. +13:08 So we have these two snakes, neither of them are venomous. +13:10 Let's book a cage. +13:12 I'm going to start by booking this. +13:13 Now, how do I know that date? +13:15 Cuz over here, we have two available bookings +13:20 for the large boa cage, +13:21 and these times one to six in January. +13:25 So we'll go two, let's say two to four. +13:27 It should be fine. +13:29 This four. +13:32 And it says which snake, remember, +13:34 it matters the size of the cage and snake, +13:37 as well as whether it's venomous. +13:38 So we'll pick slither. +13:40 And hey, look, the one cage is here. +13:41 So let's say, all right, let's book it. +13:44 We've successfully booked the large boa. +13:48 All right, now we haven't written view your bookings, +13:52 but we do have that, I believe, +13:55 we might have that for the other one. +13:57 Go over here as a host, and we log in as Michael. +14:01 I think we might not have implemented this as well. +14:04 But we can list our cages. +14:06 Yes, there we go. +14:07 We can see that we have two cages, +14:10 Bully's cage and large boa. +14:12 And look at this, somebody has booked this one, +14:16 this slot, for the large boa cage. +14:18 Yes, so it looks like that worked successfully +14:21 just like we expected. diff --git a/video_transcripts/5-building-with-mongoengine/14-demo-view-bookings-as-guest_transcript_final.txt b/video_transcripts/5-building-with-mongoengine/14-demo-view-bookings-as-guest_transcript_final.txt new file mode 100755 index 0000000..9ab0ca8 --- /dev/null +++ b/video_transcripts/5-building-with-mongoengine/14-demo-view-bookings-as-guest_transcript_final.txt @@ -0,0 +1,137 @@ +00:00 Alright we're down to the very last thing we're +00:02 going to do as a guest which is to view our bookings. +00:05 We're able to book a gauge but as a guest we couldn't see +00:09 what are your upcoming stays for your snakes +00:12 and things like that. So, again, just for the sake of time +00:15 let me go over here and put some pre-written code +00:17 and we'll go write the data access later. +00:21 So, require an account and we're going to call +00:24 "get_bookings_for_user" and this is going to return a set of +00:27 bookings and just to remind you what that looks like +00:30 the bookings are going to have the days, possible reviews and +00:33 the snake ID is going to be really important. +00:37 So what we want to do is we want to say give us the snake +00:41 given a snake ID and a super simple way for us to do that +00:44 is to actually generate a dictionary using a +00:47 dictionary comprehension. So this little expression on here +00:51 is going to create a dictionary where they key is the ID +00:54 and the value is the snake for all the snakes +00:58 belonging to us, whoever the logged in user is. +01:00 Then we're going to get the bookings and this part we're +01:03 going to write and then we're going to loop over here +01:06 and we're going to print out the snake +01:10 and we'll use the dictionary to look up +01:11 to get the name here. We're going to print the cage name +01:15 and you're going to import date/time there. +01:19 We're going to create a date/time and do a little bit +01:22 of math here on the check out. So we're going to turn this +01:25 back into day. If you're checking in on this day +01:27 for five days or something like that. +01:29 Okay so that's what our view bookings UI +01:33 pretty much stretch. Right, with our app +01:36 this is kind of the UI code, if you will. +01:39 But we've got to write this bookings for user +01:43 this is going to be a "bson.ObjectID" +01:48 We'll bring it back as a list of booking, I believe. +01:52 Now, notice one other thing before we write this code +01:56 Over here we're saying "b.cage.name" +02:00 Now cages have bookings, but bookings don't have cages. +02:04 There's not a super nice way to create that +02:07 reverse association in MongoEngine, +02:11 so what we're going to do is part of what we're going to do +02:14 in this function is we're going to set up that relationship +02:17 and let's call this "user_id" or account ID +02:20 or something like that. Okay, so +02:22 the first thing we need to do is find the owner +02:25 so close account equals let's pass in an email instead +02:31 something like that and before I forget +02:33 pass in the email there. Okay great. +02:35 We'll have our account and we've already verified +02:38 that they're logged in. So we can just assume that +02:40 that happens. So we can say booked cages and so we can +02:43 find all the cages that have been booked +02:46 by this person. So we can say "Cage.objects" +02:51 and now we'll do a few other interesting things +02:52 we haven't seen yet. Let's say filter +02:54 and here we'll say "bookings__" +02:58 and then we're looking for +03:00 the guest owner ID. Let's go to our booking +03:06 that matches the owner. That equals to "account.id" +03:14 We don't actually care about all the details about the cage +03:16 and you can skip this little part right here, +03:18 but just as a means of efficiency we go over here +03:22 and say only. We haven't talked about this yet. +03:24 What we can do is say we only want to get back +03:27 two pieces of information not potentially tons of +03:30 information in this document. We want the bookings, +03:34 and we want the name. So when we say "cage.nameabove" +03:37 that means something, right? +03:40 So let's create the bookings and we'll do this with +03:42 a list comprehension. So we'll say bookings is this +03:46 and I'm going to write it one way and then I'm going to have +03:48 to make a change to do this reverse association with +03:50 the cage. So I can say booking +03:55 or booking in the book cages +03:59 but remember there are other bookings that are unrelated +04:02 to us. Here right could be two different snakes staying +04:04 in the same cage, different days. We need a little test +04:12 alright so this is going to be the bookings that are +04:17 assigned to us within the cages for which we have booked +04:21 right, so show me all the cages where we've booked +04:23 at least one of the bookings and we're going to strip out +04:26 the unrelated ones. So you might think that we're kind of done +04:29 and that we're very, very close to done, but we're not done. +04:33 So we've run this, you should see this is going to crash. +04:37 It's that line right there. I almost messed up. +04:40 So if this is actually for cage in the booked cages +04:44 and each cage contains a booking. So we've got to do a +04:47 double loop here. Booking in "cage.bookings" +04:52 What we're going to do is going to take the tyrannical list of +04:54 cages which nested inside them contain a bunch of bookings +04:59 and we're going to flatten that list with a double loop +05:02 go through each cage, go through each booking and just +05:04 turn that into a list and across all those bookings +05:08 across all the cages only show the ones which we some point +05:11 have booked. Okay so this is close but if we try to run it +05:16 this reverse lookup of the cage here it's not going to work. +05:21 So, let's see we're going to try and run that real quick +05:24 save, go to guest login as Sarah +05:30 now if we try to view your bookings +05:32 we see no cage. So let's view one more little trick. +05:36 We can do a transformation at this level, right +05:39 this is like the select part of the list comprehension +05:42 now this has to be an expression +05:44 I don't think we can do this with a lambda expression +05:49 cause it doesn't allow us to make modifications +05:51 so we got to define this little local function +05:53 so we'll say "map_cage_to_booking" +05:59 given a cage and a booking this is going to +06:03 be the silliest thing you've seen. "booking.cage = cage" +06:08 well, and then we're going to return cage--sorry--booking +06:13 but why do we need that? We need that so we come down here +06:16 and we add this function. It's going to take a booking and it's +06:20 going to put that same booking right back into the list +06:24 but the booking will be changed in that it's +06:25 going to have a cage associated with it. +06:27 Okay, I know that's not super obvious but that's what +06:30 we need to make that one line work. +06:33 I made a quick error here, I had booking cage here +06:38 and cage booking there so cage booking, cage booking +06:42 okay looks like its ready. We'll try again. +06:55 Alright, so fewer bookings. Woo hoo it works! +06:57 We have one booking. Our snake Slither is booked in the +07:01 large boa on the stay for five days. +07:04 Let's add one more booking just to make sure this is working. +07:07 We'll book a cage, alright so let's try to book that other +07:12 available booking. This time we're going to put Bully in there +07:16 and I guess we're going to book that one. Great +07:18 we view our bookings again. We now have our snakes booked +07:20 into the different sections at the different times. +07:24 Let's just try one more time now that those bookings +07:26 should have been used up, to see what happens. +07:30 So let's try to book one more cage. Say we'll start +07:32 on that date and we'll check out on that date +07:35 and we'll use this one. "Sorry, no cages, both +07:38 available spots have already been booked." Just so happens +07:41 to be to our snakes. Awesome. +07:43 It looks like the guest side of things is 100% working diff --git a/video_transcripts/5-building-with-mongoengine/15-demo-view-bookings-as-host_transcript_final.txt b/video_transcripts/5-building-with-mongoengine/15-demo-view-bookings-as-host_transcript_final.txt new file mode 100755 index 0000000..50d223c --- /dev/null +++ b/video_transcripts/5-building-with-mongoengine/15-demo-view-bookings-as-host_transcript_final.txt @@ -0,0 +1,120 @@ +00:01 We just have one function left to write +00:04 for viewing the bookings as a host. +00:07 What bookings, available booking dates in your cages, +00:10 have been booked? +00:11 And then we'll be done with this application. +00:13 So this require account stuff, paste that, +00:16 that's the same, nothing special there. +00:19 Now we want to get the cages for the current user. +00:22 Well guess what? +00:23 We've already written that. +00:25 "find_cages_for_user(state.active_account)" +00:28 That's cool, so that was already done +00:30 at some point along the way. +00:32 And then what we need to do is we want to get +00:35 the bookings and we want actually a flat list of +00:38 these are all the available slots that +00:40 people have booked across all the cages. +00:44 All right, so what we're going to do is +00:47 something called bookings, like this, +00:48 a list comprehension. +00:50 And we're going to use sort of a dual comprehension +00:52 to flatten it. +00:53 So I'm going to put something here, one or two things, +00:57 I'll say what those are in a minute. +00:59 So we want to say "for c in cages" +01:02 And then for each cage we want to say +01:05 "for b in c.bookings" +01:08 Now we don't want all of them, right? +01:10 There's already a way to see that kind of stuff. +01:12 But what we want is the ones that have been booked. +01:15 We know they're booked if the booked time +01:17 or the booked date was set. +01:19 Let's say "if b.booked_date is not None" +01:24 What do we want to get back here? +01:25 We want to get the cage and the booking. +01:27 So we're going to get a flat list of all the bookings +01:30 and along with it we're going to carry along +01:32 its cage that it came from. +01:35 So that's pretty straightforward. +01:36 And then, that's this part, getting them as a flat list. +01:40 And then the last thing to do is just print them out +01:42 and that's a lot of typing for not a lot of value. +01:45 So let's paste that over. +01:48 We'll import date time, +01:51 and we're going to, as we loop over them, we're going to +01:54 unpack that +01:55 So the cb here gets unpacked into the cb right there +01:59 and we'll say for this cage, it was booked on this date +02:02 by so and so. +02:04 All right, looks like this is going to work. +02:05 I have a lot of faith in it. +02:06 Let's give it a try. +02:08 We'll come over here, we're going to be host, +02:10 we're going to log in as me. +02:14 And, are you ready? +02:15 Moment of truth, view your bookings. +02:18 No, there's no duration and days. +02:21 Ah, so where did this duration and days go? +02:25 So this is the booking. +02:26 Let's look at the booking real quick here. +02:28 It has a check-in and check-out date. +02:30 Let's add this duration in days. +02:33 Now if we add it as an actual thing that MongoEngine saves, +02:37 that won't be so great because it's going +02:39 to have duplicate data. +02:41 Check-in, check-out, and days, could get +02:43 out of sync. +02:43 So what we're going to do is we're going to add a property. +02:46 So we'll have a property called "duration_in_days" +02:52 So close, days. +02:54 That the same, yes. +02:55 Duration in days. +02:57 So down here we just need to use the time delta +03:00 to figure out what that is. +03:01 So we'll say "dt=self.check_out_date - self.check_in_date" +03:04 and will return +03:08 DT dot days. +03:09 All right, let's try to run this again. +03:13 Oh double ats, come on, too much help here. +03:18 Here we go, all right. +03:19 Come as a host, log as me, all right, ready? +03:22 View your bookings. +03:23 Ta-da, beautiful. +03:25 And you can see our property is working just right there. +03:29 So five days and 10 days. +03:31 Now remember when we actually checked, +03:34 we said we were going to book it? +03:36 We could book it for a sub-set of time, +03:38 we just don't store the data for how long +03:40 the user said versus how long the time slot was. +03:43 So they kind of get that whole slot, +03:45 and that slot is worth five days, +03:47 and 10 days in duration. +03:49 But given the data that we're keeping, +03:51 this is working totally well. +03:54 So I think we've done everything. +03:55 We can create a count, log in, +03:57 we can list our cages. +04:00 We can register a cage, which we already did. +04:03 We can update the availability which is +04:05 how we got these slots. +04:06 We can view our bookings, which we just wrote. +04:08 And we can even get little help +04:10 and go back to the main menu. +04:12 And we can check out the guests, just one +04:14 more time around. +04:15 Again, create log in, same thing. +04:18 It says book a cage, that's reserve a cage +04:21 for your snake. +04:22 We saw that we can add snakes. +04:24 Oh, I got to log in as Sarah, she's the one with snakes. +04:29 She has those snakes there. +04:30 She can view her bookings from the perspective +04:33 of her snake, not from the available slots, +04:36 things like that. +04:37 She only sees her bookings, not all the +04:39 bookings across everything. +04:41 And we can go back to the main menu, +04:42 or 'cause we're done, we can say goodbye +04:45 to Snakebnb diff --git a/video_transcripts/5-building-with-mongoengine/16-concept-inserting_transcript_final.txt b/video_transcripts/5-building-with-mongoengine/16-concept-inserting_transcript_final.txt new file mode 100755 index 0000000..7ac4cda --- /dev/null +++ b/video_transcripts/5-building-with-mongoengine/16-concept-inserting_transcript_final.txt @@ -0,0 +1,30 @@ +00:01 Now we built our app but let's review +00:02 some of the core concepts we saw along the way. +00:05 If we want to insert an object +00:07 we just create a standard Python object style. +00:11 We just say owner = owner. +00:13 Called the initializer. +00:15 We could either pass the values as keyword arguments +00:18 or we could say owner.name = name. +00:20 Owner.email = email. +00:22 And at this moment there is no ID associated +00:25 with this object. +00:26 But then we just call owner.save() +00:27 and now the object ID or whatever the primary key is +00:32 we can set functions to be called +00:33 when that happens for that generation. +00:36 Whatever that's going to be. +00:37 We've got it set after you call save, +00:40 so now you can start working with it +00:41 as if it came from the database. +00:43 We also might want to insert a bunch of things. +00:46 It turns out, if you have 100,000 items to insert, +00:49 and you create one save, create one called save, +00:52 create one called save, it's a lot of database +00:55 back and forth, and it's very slow. +00:56 So what you would rather do is create a list of them. +00:59 So here we have a bunch of snakes we want to save, +01:01 we create a bunch of them, put them in this list, +01:03 and then you call snake.objects().insert(snakes) +01:05 and you give it the list, and that's much quicker +01:07 if you want to do a bulk insert type of thing. diff --git a/video_transcripts/5-building-with-mongoengine/17-concept-queries_transcript_final.txt b/video_transcripts/5-building-with-mongoengine/17-concept-queries_transcript_final.txt new file mode 100755 index 0000000..ddbb5c5 --- /dev/null +++ b/video_transcripts/5-building-with-mongoengine/17-concept-queries_transcript_final.txt @@ -0,0 +1,22 @@ +00:00 We sound like querying the database +00:01 is pretty straightforward. +00:02 The way it works is we start +00:04 with the collection, +00:05 or the type that represents the collection, +00:07 we want to work with. +00:08 And then we say ".filter" +00:10 so here we're saying filter, +00:11 and we want to do a match, where the email +00:14 equals the value of the email variable. +00:16 And you could have more than one thing in here. +00:18 You could have more than one filter statement, +00:19 and those basically combine as an and. +00:23 Now this would return +00:25 potentially a bunch of owners, +00:26 but we don't want a bunch of owners. +00:29 We want the one that we know matches this email, +00:31 so we can say ".first" +00:32 and it'll give us the one, or at least the first, +00:34 item to match. +00:36 If there's no match, we get none back. +00:38 We don't get a crash or anything like that. diff --git a/video_transcripts/5-building-with-mongoengine/18-concept-querying-subdocuments_transcript_final.txt b/video_transcripts/5-building-with-mongoengine/18-concept-querying-subdocuments_transcript_final.txt new file mode 100755 index 0000000..50c12f0 --- /dev/null +++ b/video_transcripts/5-building-with-mongoengine/18-concept-querying-subdocuments_transcript_final.txt @@ -0,0 +1,33 @@ +00:01 When we're creating against just plain, straight fields +00:04 in our object, that's pretty straightforward. +00:06 We said email equals the value that we're looking for, +00:09 but if we're looking deep inside of a hierarchy, +00:12 it can get a little more, let's say, not obvious. +00:15 So we're combining two really interesting things here. +00:17 We're going to the cage and we're going to search within +00:20 the bookings embedded document list, right? +00:25 Bookings is a list and it contains a bunch +00:27 of these booking objects. +00:29 So the first thing that we're doing is using double +00:30 underscores say bookings, guest, snake ID. +00:33 So we're looking at the value guest snake ID of the booking +00:36 items within that list and we're also applying +00:40 the N operator. +00:41 So the double underscore N means the collection +00:45 on the right, we're doing an N test for the various booking +00:49 and the snake ID. +00:51 So we use the double underscore to separate and navigate +00:53 the levels in subdocuments as well as to apply +00:57 the particular dollar operators and then what I consider +01:01 best practice is to fully execute the query before +01:05 you leave this function. +01:06 If we just returned book cages, it kind of still +01:09 is not yet executed. +01:10 It hasn't quite talked to the database and so I want +01:12 the database to be done and over with by the time +01:14 we leave this method. +01:15 So wrapping it in a list will execute all that stuff +01:19 and pull it back. +01:20 For super large amounts of data, there might be reasons +01:22 you don't do this, but for most of them, I would do +01:26 something like this. diff --git a/video_transcripts/5-building-with-mongoengine/19-concept-querying-operators_transcript_final.txt b/video_transcripts/5-building-with-mongoengine/19-concept-querying-operators_transcript_final.txt new file mode 100755 index 0000000..c8cb6a7 --- /dev/null +++ b/video_transcripts/5-building-with-mongoengine/19-concept-querying-operators_transcript_final.txt @@ -0,0 +1,19 @@ +00:00 Now, sometimes you don't want a quality in your matches. +00:03 You want some kind of operator like greater than +00:05 or less than or in or things like that. +00:09 So here you can see, we're going to the cage +00:11 and we're finding all the cages +00:13 where the square meters is at least the minimum size. +00:16 So just like with subdocuments, we use the double underscore +00:19 and the operator name, GTE, here +00:22 to actually do this query. +00:24 There's a bunch of dollar operators. +00:25 You can find them in the MongoDB documentation, +00:27 and you apply all of them in this way. +00:31 Now, the other thing that we're looking at here +00:32 that we didn't do in our app is count. +00:35 So if we want to know how many cages there were, +00:37 we could say do the query.count, +00:39 and it'll just do a count in the database +00:41 rather than pull all the objects back +00:44 where you do a length of it or something like that. diff --git a/video_transcripts/5-building-with-mongoengine/20-concept-updating-via-docs_transcript_final.txt b/video_transcripts/5-building-with-mongoengine/20-concept-updating-via-docs_transcript_final.txt new file mode 100755 index 0000000..03af5df --- /dev/null +++ b/video_transcripts/5-building-with-mongoengine/20-concept-updating-via-docs_transcript_final.txt @@ -0,0 +1,37 @@ +00:00 The most straightforward way +00:01 to make changes to a document, or to a record +00:04 is to go get it from the database, +00:06 change the class, and call save. +00:08 So in this example, +00:09 we're going to go get the owner out of the database. +00:11 Make sure that's all good, don't want to have errors. +00:14 We're going to create a snake, +00:15 we're going to do work with the snake, +00:17 and then we want to append the snake ID +00:21 onto the owners snake ID collection. +00:25 So on line 11 we say owner.snakeIDs.append, +00:28 and we give it this new ID that was gotten +00:31 from the snake on line nine, when we called save. +00:33 And we save the owner, and that's that. +00:36 So we get the document, we make a change to it, +00:38 in the case of line 11 here, and then we just call save +00:40 and that pushes it back. +00:42 This works, but that transfers the entire document +00:45 out of the database, over to our app, +00:47 deserializes it, processes it, +00:49 and then reverses that back to the database. +00:52 That can be slow, +00:53 but that can also have concurrency issues. +00:56 If two people run this exact same method +00:59 at almost exactly the same time, +01:01 with the same email address, there's a chance +01:04 that one save is going to overwrite +01:06 the snake ID's of the other, right. +01:08 Both of them read the owner, +01:09 one makes a change, one makes a change, +01:11 one saves, the other saves. +01:12 You only have one snake, not two. +01:14 So there are challenges with this, +01:16 but if you're pretty confident that that's not an issue +01:18 you're going to run into, this is a really nice +01:20 and easy way to do it. diff --git a/video_transcripts/5-building-with-mongoengine/21-concept-updating-via-operators_transcript_final.txt b/video_transcripts/5-building-with-mongoengine/21-concept-updating-via-operators_transcript_final.txt new file mode 100755 index 0000000..399ace6 --- /dev/null +++ b/video_transcripts/5-building-with-mongoengine/21-concept-updating-via-operators_transcript_final.txt @@ -0,0 +1,57 @@ +00:00 While MongoDB and MongoEngine +00:02 let us work with these documents, make changes, +00:05 and push them back in, there might be a better way. +00:08 So, if we know that we want to just change +00:11 some small part of the document in a very controlled way, +00:14 we might want to use some of the atomic operators. +00:16 So here let's suppose that there's a number of stays field, +00:21 which is an integer, in the cage. +00:24 So we just keep track of how many times +00:25 people have stayed at it. +00:27 Now there probably are better ways to get this, +00:29 but let's suppose that is a number +00:31 and we want to increment it. +00:32 Instead of pulling the cage back +00:34 and doing a plus equals one sort of thing and saving it, +00:36 we can literally go to Mongo and say +00:39 increment this number of value of number of stays by one. +00:43 That could be negative, that could be ten, +00:44 but I have one here. +00:46 So you write the "query.update_one" +00:49 And then pass a little operator. +00:50 So that's really great. +00:52 Now how about this putting the snake ID on the owner. +00:55 We can do that too. +00:57 Over here we're adding a snake. +00:59 We generate the snake we call save. +01:01 That's standard, that's an insert. +01:02 But then this line where you have the number of updated, +01:06 we have owner, objects, emails, email, +01:08 and then instead of saying get it back, +01:11 make the change, append it to the snake ID as impulse save, +01:15 we're doing something with an operator. +01:17 We're saying update_one, and we're using the push operator. +01:20 That's $push. +01:21 We're going to push it onto the snake IDs collection. +01:25 Another thing we might do, +01:26 which probably makes even more sense, +01:28 would be add to set. +01:29 It's another related operator that will say +01:32 add this ID to this set or this list +01:35 if and only if it doesn't already exist +01:36 so you won't get duplicates. +01:38 So we're pushing the snake ID on there +01:41 that way we never pull the owner back. +01:43 These are atomic. +01:44 They're perfectly fine in concurrent situations, +01:46 things like that. +01:47 So this time we have to check that we updated it +01:49 in a different way, but the same effect as we saw before. +01:54 So MongoEngine supports these in place updates, +01:57 array operations, and set operations. +02:00 So increment as well as push, +02:03 and these are both better for concurrent safety. +02:05 Think of them as basically transactional +02:07 and they're better in pure performance. +02:09 They're not always as simple to work with, +02:11 but they are better if you can use them. diff --git a/video_transcripts/5-building-with-mongoengine/3-register-connections_transcript_final.txt b/video_transcripts/5-building-with-mongoengine/3-register-connections_transcript_final.txt new file mode 100755 index 0000000..35797b8 --- /dev/null +++ b/video_transcripts/5-building-with-mongoengine/3-register-connections_transcript_final.txt @@ -0,0 +1,83 @@ +00:00 The first thing that we need to do +00:01 to start working with MongoEngine is +00:03 tell MongoEngine how to speak to MongoDB. +00:06 Let's go back to our Snakebnb app we've been working on. +00:09 And we're going to go in here to this data section. +00:12 we're going to create a new file, +00:14 a new Python file called mongo_setup. +00:16 So in here, we're going to write a simple method +00:19 that we can call from other places, +00:20 and as you'll see in the slides that we look at later, +00:24 for real applications that use proper connections, +00:28 encryption, accounts, things like that, +00:29 this can be a little more complicated, +00:31 but it's going to start out pretty simple, +00:32 so we'll say, "global_init()" and in here, +00:35 we just want to call one function, +00:37 so we're going to have to have MongoEngine here, +00:40 so we'll import MongoEngine, +00:41 and we'll just say, "mongoengine.register_connection()" +00:44 and the first thing that we pass is an alias, +00:47 so I'll be real explicit and say, "alias='core' " +00:50 Now what is this alias thing about? +00:51 We can have multiple connections to even multiple databases, +00:55 or even multiple database servers registered here. +00:58 We could have like a core data and +01:01 say analytics for just analytics that goes to a +01:03 separate database that maybe has tons more data +01:06 because it's page views, and actions, and so on. +01:08 But we might keep that separate, so we can back it up +01:10 on a separate schedule, something like that. +01:12 Then, we need to set the name of the database, +01:15 and we'll set that to Snakebnb. +01:17 Do a quick format here, and it's all good to go. +01:20 Like I said, this gets more interesting in real connections. +01:23 We're going to need to call this to talk to MongoDB, +01:27 so let's go over to our little program, +01:28 and you saw up here at the top, there was this to do, +01:31 setup MongoEngine global values, +01:32 and that was basically what we were doing there, +01:35 so we need to come over here and say, +01:36 you need to go to "data.mongo_setup" +01:39 We'll just call it "mongo_setup" +01:42 And this should be pretty simple, +01:43 mongo_setup.global_init() +01:45 Now we just need to make sure we call this once +01:47 in our application and we need to do this +01:49 before we actually interact with anything else, +01:52 so let's go and apply these settings +01:54 over to our entities as well, so we'll look at snakes first. +01:59 Now this model is going to be mapped +02:01 into one or more of those databases. +02:04 Well, one among many potential databases, +02:06 so the way that we can tell it how to work, +02:09 MongoEngine will use a property we can add to it. +02:12 We can say "meta = " and we give it a dictionary, +02:14 and we can say " 'db_alias' " +02:17 and we'll say " 'core', " here, okay? +02:18 While we're at it let's say, " 'collection': 'snakes' " +02:22 So even though we called it capital S snake, +02:25 the thing in the database where these records, +02:27 these documents are stored will be called +02:29 snakes, plural, lowercase, and here we can tell it +02:31 if this goes into the core database, +02:33 unlike maybe the analytics one, or something like, +02:36 all right, we're only going to have one in here, +02:37 but for our example, you want to have this here, +02:39 in case you want to add more later. +02:41 All right, I'll go ahead and add the rest of these +02:43 to owners and cages, but not bookings, +02:46 because bookings is going to be nested inside here, all right, +02:49 so we don't need to tell that how it gets stored, +02:51 'cause it's stored alongside cage, +02:54 triple set of cages, and no surprise, owners. +02:57 All right, so those are our three top-level entities +03:00 that map to our three top-level collections in MongoDB. +03:02 Now, we've registered the connection +03:04 by using our Mongo Setup Global, +03:06 which should we just call a regular connection. +03:08 Like I said, this shuouldn't get +03:09 way more complicated in reality, and then, +03:11 we just go and we set this meta to use the core connection +03:14 as well as naming the actual table +03:16 or collection it's going to go to. diff --git a/video_transcripts/5-building-with-mongoengine/4-concept-register-connections_transcript_final.txt b/video_transcripts/5-building-with-mongoengine/4-concept-register-connections_transcript_final.txt new file mode 100755 index 0000000..c6c3a0d --- /dev/null +++ b/video_transcripts/5-building-with-mongoengine/4-concept-register-connections_transcript_final.txt @@ -0,0 +1,51 @@ +00:00 Let's take a moment and look +00:01 at this concept of registering connections. +00:03 We're going to need to work with MongoEngine, +00:05 so of course, "import mongoengine" +00:07 and we need to set the alias and the name of the database, +00:10 so it should say "mongoengine.register_connection" +00:12 give it the alias, give it the name. +00:14 We need to call this before we start interacting +00:16 with our classes and other types, before we try +00:19 to do any queries or save any data, this has to be set up. +00:22 So, this is what we wrote in our application +00:23 and this works fine when you're talking to +00:25 the local MongoDB running no encryption, no accounts, +00:29 default port running on the local machine. +00:31 Same machine that this could. +00:32 Now, if you're doing this in production, there's more to it. +00:35 You need to set the username and password, +00:37 which you have to set up in MongoDB, +00:39 there's none by default, so you got to +00:41 set that up with the right permissions. +00:43 Probably it's a different server on +00:44 an alternative port, so set the host and the port. +00:47 You would like to create some sort of admin account, +00:50 which is associated with that username and password +00:52 so you say, "Look you authenticate an admin, +00:54 use this mechanism." +00:56 Finally, if you're going to do connections across, +00:59 somewhere outside your data center you pretty much +01:01 should just turn this on, you need to turn on SSL +01:03 and configure the server for SSL. +01:05 And then you pass that additional data +01:07 in addition to the alias and the DB. +01:10 Now, there's a lot going on here, and deployment +01:12 and running MongoDB and production +01:14 is not as simple maybe, as it could be. +01:16 Certainly it's something that you need to be very careful +01:19 about, like no authentication, no encryption, right? +01:22 Don't run your code that way, it's fine to do it +01:24 for development, but don't do it for production. +01:27 In my full MongoDB course I actually go into, +01:30 spend an hour, go and create a Linux server +01:32 and set it up in a cluster of the database +01:34 and the web servers and those kinds of things +01:36 and really make this work perfectly and safely. +01:39 But, in this course we're not going to go into it, +01:41 I just want to leave you with you need to set this up +01:43 you can look at the MongoDB.org site as well, +01:46 and MongoDB.com site and go through +01:47 the documentation on some of the steps. +01:49 Or, just take my other course if you're +01:51 really going to go and use this in production. diff --git a/video_transcripts/5-building-with-mongoengine/5-mongoengine-entities_transcript_final.txt b/video_transcripts/5-building-with-mongoengine/5-mongoengine-entities_transcript_final.txt new file mode 100755 index 0000000..d7376cc --- /dev/null +++ b/video_transcripts/5-building-with-mongoengine/5-mongoengine-entities_transcript_final.txt @@ -0,0 +1,156 @@ +00:03 So far we've modeled our data with Python classes, +00:05 but there was no real MongoEngine entity stuff, +00:08 they wouldn't actually be saved or processed by MongoEngine. +00:11 It just happens to be we kind of sketched +00:13 them out in Python class style. +00:15 So we're going to change that now. +00:16 We're going to go make our standard, plain old Python classes. +00:19 Proper MongoEngine entities. +00:21 The snake is pretty simple. +00:22 So let's start there. +00:23 So in order to work with MongoEngine over here, +00:26 I'm going to have to import it. +00:28 Now, you might want to do +00:30 from MongoEngine, import some things, +00:32 but I like to be real explicit. +00:34 These things are coming from MongoEngine, +00:36 even in my production code. +00:37 So this is how I'm going to do it. +00:38 We're going to set the register date +00:40 to a particular type of descriptor +00:42 that comes from MongoEngine, +00:44 and at sort of the type of level this tells MongoEngine +00:47 what type of data, and constraints, +00:49 and requirements go onto this field. +00:52 However, at run time, it's going to act like, +00:54 say date time or whatever it is. +00:56 In this case the date time +00:57 and species would be string and so on. +00:58 So we'll come over and say "mongoengine.DateTimeField()" +01:02 and we'll just go like this. +01:03 This will tell MongoEngine to map +01:04 that to the database as a date-time field. +01:06 Over here, we'll say "mongoengine.StringField" +01:09 and over here, the length, +01:11 let's say this is in meters and that's probably decent floats. +01:15 alright so this will be a float field. +01:17 The name, again is string field. +01:19 Whether it's venomous or not, that's true or false. +01:22 It could be a number, like level of venomous. +01:24 I don't know, but we're going to call this a boolean field +01:27 and that's that. +01:27 So our snake is all ready to map into MongoDB. +01:31 MongoDB doesn't have things like required fields +01:34 or default values or anything like that, +01:36 but MongoEngine does. +01:37 So let's change this to make it a little simpler +01:39 to create a snake. +01:40 So for example, "registered_date" is almost always just +01:43 whenever you inserted it, right? +01:45 So what we can do is we can come over here +01:46 and set a default function +01:47 that will execute any time MongoEngine inserts a new snake. +01:50 So we're going to start with date-time. +01:52 So the function that we want to call is the now function +01:55 which gives us the full year, month, day, +01:58 hour, minute, second representation of time. +02:00 So we'll come in here, say "datetime.datetime.now" +02:03 and be very careful not to put the parenthesis. +02:06 You're passing the function, not the value of "now" +02:08 That would be when the program started. +02:10 So that can be a little tricky. +02:11 Over here for species, let's say that you have +02:13 to say the species. +02:14 So we're going to say this is going to be required is true. +02:17 In fact, the length is required, the name is required, +02:20 whether it's venomous is required. +02:22 We can have things like minimum values have to be like 0.001 +02:27 or things like this. +02:29 So you can't have like a negative length. +02:30 There's a lot of cool constraints +02:32 that we can do with our types here. +02:33 So this snake is now ready to be used in our database. +02:38 Let's look at the next one. +02:39 Let's work on the cage next. +02:41 So again, import MongoEngine and we'll use that +02:43 in a few places. +02:44 This is exactly the same here. +02:46 So set the default and then the name is +02:48 just going to be string and so on. +02:50 So I'll just sketch these out for you. +02:53 So these seem like reasonable types here, and let's go ahead +02:55 and set the required properties for things that we require. +02:59 Most of these would be required, +03:01 and whether or not we allow dangerous snakes. +03:03 If you don't set that, let's say no, +03:05 by default you're not going to have a dangerous snake. +03:08 Okay, so these are just like the snake before. +03:10 This however, gets more interesting. +03:12 We're going to come down here and we're going to set this +03:14 to be a "mongoengine.EmbeddedDocument" +03:18 We can have just a single thing, like a booking +03:20 or embed the snake in the owner or something like that, +03:25 but we want to have a list of embedded documents +03:28 and what we need to pass in here is the actual type +03:32 that is contained in there. +03:32 So we're going to import "data.bookings.Booking" +03:35 One other thing that I also realized that I forgot to do +03:38 in the previous ones and we'll go back and fix that, +03:39 is we need to tell MongoEngine +03:41 that this is a top level document. +03:43 We need to make this have a base class of type document. +03:47 We'll do that for snakes as well. +03:54 Now let's go to the booking, we were just working with that. +03:56 So this one, recall, is the type +04:00 that's embedded within the cage +04:03 That's embedded in the cage, that means it's not a document. +04:06 Right? That would be a top level thing. +04:07 This is an embedded document. +04:09 Alright, so this can be contained within other documents, +04:11 but itself cannot be top level. +04:14 Let's go ahead and set these, as well. +04:18 Now, when we're talking about IDs in MongoDB, +04:20 the default is something called an object ID. +04:23 Like a UID, or GUID or something, +04:25 so when we're talking about a reference, +04:27 typically it doesn't have to be, but it typically is. +04:33 There we go. +04:34 We've got our two references as object IDs. +04:37 We've got our booking date, +04:38 which does not have to be required, +04:39 and it doesn't have a default value. +04:41 This is when was booked, +04:42 which happens after the booking slot was made available, +04:47 but at the time of creation of the slot of booking, right, +04:50 we've like put a note for booking. +04:51 You have to say the check in and check out date, +04:53 and again, the reviews. +04:55 These are not getting set until after. +04:58 I set this to zero so we can say, +05:00 like, you know, required to be one to five. +05:03 They actually rate the thing and then you +05:05 can sort of exclude the ones that are zero. +05:08 The final one is owner and it's very, very similar. +05:10 I'll just sketch that out for you. +05:13 We've got our flat pieces here. +05:15 Our register date, name and email +05:17 and now we're going to have a list of IDs. +05:19 So we'll come in here and say "mongoengine.ListField" +05:23 for both of them. +05:26 So this'll let us store the object IDs +05:28 that refer to the snakes and the object IDs +05:30 that refer to the cages. +05:32 Last thing to do is make the base class the document here. +05:36 Alright, so what have we done? +05:38 We've set all of the fields to their respective types +05:41 out of MongoEngine as descriptors. +05:43 We've set either default or required values +05:46 and we've set the metadata, +05:48 which talks about which database connection to use +05:50 and what to call the collection +05:52 when it goes into the database. +05:54 And we've done that for our three top level items here. +05:58 The one that is different and stands out is the booking, +06:00 which is embedded within the cage +06:03 and this is an embedded document, +06:06 but otherwise everything goes pretty much the same. diff --git a/video_transcripts/5-building-with-mongoengine/6-concept-mongoengine-entities_transcript_final.txt b/video_transcripts/5-building-with-mongoengine/6-concept-mongoengine-entities_transcript_final.txt new file mode 100755 index 0000000..9862293 --- /dev/null +++ b/video_transcripts/5-building-with-mongoengine/6-concept-mongoengine-entities_transcript_final.txt @@ -0,0 +1,91 @@ +00:02 Let's review the core concepts around +00:04 creating MongoEngine entities. +00:06 We started out by creating what I call basic classes. +00:09 These are classes that could just as easily have been mapped +00:12 to a relational database, +00:14 because you just have flat fields or columns, +00:16 if you want to think of them that way. +00:18 And none of the nested or particular capabilities +00:22 of document databases. +00:24 The one that matched that was the snake. +00:26 The snake, we make sure that it derives +00:28 from "mongoengine.Document," +00:30 and then we specify the fields by +00:33 passing along or creating these MongoEngine descriptors. +00:36 So we said there's a registered date, +00:38 and that's a mongoengine.DateTimeField. +00:40 The length, that was a float. +00:41 That was the length of the snake in meters. +00:43 The name of the snake is a string, +00:45 species has a string as well, +00:47 and whether or not it's venomous, +00:50 is a Boolean, true or false. +00:52 So you can see we can map out the types +00:54 for this basic snake class, really easily here. +00:57 Of course our snake should have default values, +01:00 constraints like required fields, and things like that. +01:03 So here we've taken that same snake class, +01:06 and we've added a default value for the register date. +01:09 We said just call the function "datetime.datetime.now" +01:12 anytime you insert a new snake. +01:14 So it's going to automatically tag that new entity, +01:18 or that new document, +01:19 with the date in which it was inserted. +01:21 Now remember, be super careful. +01:23 Do not call the function now, +01:24 pass the function now. +01:26 Okay, we also set the length to a float. +01:29 We said that's a required float. +01:31 You have to specify the length +01:32 or MongoEngine will give you an error. +01:35 So you can't insert this thing; that field is required. +01:37 It's interesting that that's not +01:39 a feature of MongoDB, that's a feature of MongoEngine. +01:41 So by using MongoEngine instead of, say, PyMongo, +01:44 we get these additional features, same for the default. +01:47 And name, species, and venomous, +01:49 also these are all required, so we can do this here. +01:52 Now again, this is still one of these sort of basic +01:53 classes with just our constraints and defaults. +01:57 Let's look at the cage. +01:58 The cage takes better advantage +02:01 of the document database. +02:03 So we have the name, the price, the square meters, +02:07 required standard stuff there. +02:08 We also have the bookings. +02:10 These are either the times in which a cage can be booked, +02:13 or an active booking where a snake has registered +02:16 to be there at a certain time. +02:18 We model that through the booking class, +02:20 and we said this cage is going to embed +02:23 the bookings into it. +02:24 So to do that, +02:25 we use the MongoEngine Embedded Document List Field. +02:28 So a list of embedded documents, +02:31 and the argument would pass +02:32 as the type of embedded document. +02:33 So it's a booking that we'd put in with this list. +02:38 How does this look if we populate this cage +02:42 we add a couple bookings and we call save? +02:44 It looks like this. +02:46 It has the standard fields, right, +02:48 like an autogenerated_ID, the date that was registered, +02:52 this is set as a default value in the full class. +02:55 We have the name, the price, the square meters, and so on. +02:58 So that's all standard stuff, and we've seen that before. +03:01 But the bookings part, check that out. +03:03 So we have bookings, and it's a list, right? +03:06 Square brackets, not technically an array, +03:08 in JavaScript, right? +03:10 And the items in this list are those bookings. +03:14 We have a check-in date, check-out date in a range. +03:16 We have added two bookings in here. +03:19 Now we didn't fill out the, they're not booked, +03:22 we don't have a guest snake, and an owner ID, +03:25 and they haven't already taken them, +03:27 so they haven't rated it or given a review. +03:29 Some of the pieces are not saved into the database +03:31 to save space. +03:32 Nonetheless here we have our embedded bookings inside +03:36 of our document and we did that through +03:38 the Embedded Document List Field. diff --git a/video_transcripts/5-building-with-mongoengine/7-demo-create-account_transcript_final.txt b/video_transcripts/5-building-with-mongoengine/7-demo-create-account_transcript_final.txt new file mode 100755 index 0000000..6ca02f8 --- /dev/null +++ b/video_transcripts/5-building-with-mongoengine/7-demo-create-account_transcript_final.txt @@ -0,0 +1,187 @@ +00:00 We have our models, our classes in place. +00:04 We have MongoEngine registered +00:06 and told to connect to the default values +00:09 for all the local stuff for MongoDB running locally. +00:13 I already have MongoDB started up and running. +00:15 Check out the documentation on how to get that working +00:17 on your operating system. +00:18 Like I said at the beginning. +00:20 And it's time to create an account, logins, +00:24 basically start implementing all these actions. +00:26 And now we'll really get to start programming +00:29 with the entities we've designed in MongoEngine. +00:32 So let's begin by going down here to create account +00:35 and program host. +00:36 And we're going to be able to use this +00:37 for actually both the host and the guest. +00:41 Alright, so you can see there's a couple things +00:42 we have to do, and then we're going to create the account. +00:44 So let's work on this get name and email first. +00:46 So we'll say "name= " +00:47 and we'll just use simple input stuff. +00:50 "What is your name?" +00:53 Something like this. +00:54 And we'll do email. +00:58 We should of course have them, you know, +00:59 give us a password, and things like that. +01:01 But this is not a real website. +01:02 We're not really actually logging in, +01:04 it's just sort of a user creation-type thing. +01:07 So we're going to create that account. +01:08 Now I could go write the MongoEngine code +01:10 to talk to Mongo and do the inserts here. +01:13 But you'll see that we can do much, much better +01:15 if we isolate all of these behaviors +01:18 within a central location +01:21 that we can use throughout our application. +01:23 In these NoSQL databases, +01:25 these document databases, +01:27 there's not much structure in the database. +01:30 We already have some structure added +01:32 by having our classes, our MongoEngine types +01:35 that we work with. +01:36 We can also do a little bit better +01:38 by having like a centralized data access piece. +01:41 That's what we're going to work with here. +01:44 Let's go create something called a "data_service" +01:47 And we'll just put a bunch of functions +01:49 that we need to work with here. +01:51 Let's go back and let's import this, +01:53 and I'm going to import in a little short way. +01:55 So we'll say "import_services.data_service as svc" +02:02 We're just going to use the functions of that module +02:04 by calling it that. +02:05 We come back down here. +02:07 Instead of saying not implemented, +02:09 let's say this. +02:12 "svc.create_account()" +02:15 and we're going to pass the name and the email. +02:18 What we're going to get back is an account. +02:20 So we want to actually store that +02:24 in the statefulness of our application +02:26 again, in a web-app, this would be with cookies +02:28 and we get it back from the database every time. +02:30 But we have this state, which has an active account. +02:34 So what we're going to do is we're going to come up over here +02:37 and we're going to say "state.active_account = " this. +02:43 So we're going to get back an account from here. +02:46 Now PyCharm says, +02:47 whoa, whoa, whoa, there's something going on here. +02:48 There's no account. +02:49 There's no method called create account or function, +02:52 but if I hit Alt+Enter, +02:55 it'll say do you want to create one? +02:57 Of course we want to create one. +02:58 So name, let's even give this a little bit of typing here. +03:03 Say it's going to return an owner. +03:07 Okay, so that's all well and good. +03:09 Now we need to use this. +03:11 So now we get to programming with MongoEngine. +03:13 How do we create one of these owners? +03:14 Well, how would you do it if it was a regular class? +03:18 You would say this. +03:20 And you would set some properties like name equals name, +03:23 owner email is that, +03:28 and now we want to put it in the database. +03:30 So we do that by calling using +03:32 what's called the Active Record Design Pattern. +03:34 We'll just call save right on this. +03:36 Now we want to return owner. +03:39 Now the important point here, +03:40 is when we call save, all the default values are set. +03:43 And we call save the primary key the _id +03:47 is automatically generated. +03:48 Here, it's just ".id" at MongoEngine. +03:52 But in the database level, it's _id. +03:54 That's automatically set. +03:55 So this thing is up and running. +03:56 We should have everything working well here. +04:00 So let's go ahead and try to run this +04:02 and see if everything's hanging together. +04:04 Let me run it over, like this. +04:09 So here's our Snakebnb. +04:10 We're going to go and say we're our host. +04:12 Notice at the prompt here, +04:13 this little yellow thing, +04:15 there's no name. +04:17 So we'll go and create an account, +04:19 with my name, my name is Michael, +04:21 and my email is michael@talkpython.fm. +04:25 Boom, logged in. +04:26 You can see now the prompt has my logged in name. +04:30 The next thing we got to do is just go from top to bottom. +04:33 Let's go and log in. +04:34 However, there is a problem. +04:36 What if I say I want to create an account, +04:38 and I say my name is Michael2, +04:40 and I say it's michael@talkpython.fm. +04:45 If I hit enter, there's just two of those. +04:47 That's bad. +04:48 So, what we want to do is +04:50 we want to do a little check over here. +04:55 So this is great, we got this working, +04:56 and let's go ahead and annotate the type here as well. +04:59 Let's say this is an owner. +05:02 That's going to let us, when we interact with it later, +05:05 say things like this and get, you know, +05:08 all the IntelliSense and what not, +05:09 Snake IDs, whatever. +05:11 Okay, now before we do this, +05:13 we want to verify that the account doesn't exist. +05:15 So we'll say old account, +05:19 let's say find an account by email. +05:20 And again, this doesn't exist, +05:22 so we'll create this function over here. +05:25 This will let us see how to query, right? +05:27 So to insert, we create one of these and we call save. +05:31 To do the query, we're going to do this, +05:34 we'll say the "owner = " we work with type +05:37 and we say objects. +05:39 Now there's a couple of things we could do. +05:41 We could say filter, we kind of lose autocomplete here, +05:44 but that's fine. +05:44 We could say filter, and we could say "email=email" +05:48 So we would match one of the fields there. +05:53 Alright, and we would not put that of course. +05:55 And this is going to return a query, +05:57 and we want just one of them. +05:59 So we'll save first. +06:00 Now, it turns out when you have just one filter statement, +06:03 you can actually condense it down like this. +06:05 So we'll go ahead and write that. +06:07 And we'll just say "return Owner" +06:08 Okay, so there's our find account by email. +06:11 And we'll check if old account, +06:15 it'll be none if it's not found. +06:17 So if there's old account will print a few functions, +06:22 error message, success message, +06:23 with some coloration, we'll say, +06:26 "ERROR: Account with email already exists." +06:31 And let's make this a cool Python 36 F String. +06:38 Like so. +06:40 Of course, we want to bail, +06:41 and we don't want to actually create it. +06:43 Here, we could maybe do something like that print. +06:46 Let's do the success created new account with id. +06:53 And let's say "state.active_account.id" like so. +06:59 Great, let's just run this one more time. +07:06 We want to come is as a host, +07:08 we'll create an account, +07:09 let's call this Sarah. +07:11 So Sarah wants to come in, +07:12 and maybe she's going to be able to like, +07:14 she's going to be a guest. +07:16 But right, we're going to use this host path +07:19 to do it for a second. +07:19 And so "sarah@talkpython.fm" +07:24 Great, we've created a new account. +07:25 Now let's just test this thing again. +07:28 So we'll say I want to create an account again. +07:30 Sarah, it didn't actually matter, +07:32 let's say Susie and it's "sarah@talkpython.fm" +07:38 This should no longer work. +07:39 It should go and the query database and find this, +07:41 and no, no, no error an account email "sarah@talkpython.fm" +07:45 already exists. +07:46 Perfect. +07:47 I think the create account is done. diff --git a/video_transcripts/5-building-with-mongoengine/8-demo-robo-3t_transcript_final.txt b/video_transcripts/5-building-with-mongoengine/8-demo-robo-3t_transcript_final.txt new file mode 100755 index 0000000..501b492 --- /dev/null +++ b/video_transcripts/5-building-with-mongoengine/8-demo-robo-3t_transcript_final.txt @@ -0,0 +1,32 @@ +00:00 So we saved some data, and it looked like +00:02 it went into the database, right? +00:03 When we successfully saved it, we saw an idea was generated. +00:07 When we tried to log in with the same, +00:08 or create an account with the same email, +00:10 we got an error node that it already exists, +00:11 So, it's definitely working, but let's look at the data. +00:14 With my favorite tool for working with MongoDB, +00:18 Robo 3T, it use to be called Robomongo. +00:21 Robo 3T, so here it is, you can see +00:24 just by virtue of calling save +00:26 that actually connected to the database, +00:28 created this particular ... connected to the server +00:31 created this database and the various collections. +00:34 We only called save on owners, we only have owners so far. +00:38 Nevermind the fact that we created the other types. +00:40 We haven't saved anything there so, it doesn't exist. +00:43 Wwe can come down here, we can say view documents +00:45 and we actually have the two documents here. +00:47 Remember, we created two accounts. +00:49 We can view them this way, and there you go. +00:52 We have Michael and my email, Sarah and her email, +00:55 the registered dates and times +00:57 and we don't yet have a snake or a cage cause, +01:01 because well, we haven't implemented the ability to do that. +01:03 But, we're going to have snake IDs and cage IDs +01:05 in here as we create these snakes and cages. +01:08 This is a really great way to work with the data +01:10 if you go back to this mode you can even edit it, +01:14 and make changes in here if you really wanted to. +01:17 Alright, so definitely recommend installing this. +01:19 It works on all the platforms, it's free and it's awesome. diff --git a/video_transcripts/5-building-with-mongoengine/9-demo-login_transcript_final.txt b/video_transcripts/5-building-with-mongoengine/9-demo-login_transcript_final.txt new file mode 100755 index 0000000..8ae764b --- /dev/null +++ b/video_transcripts/5-building-with-mongoengine/9-demo-login_transcript_final.txt @@ -0,0 +1,38 @@ +00:01 We're able to create our account. +00:02 So let's now add the ability to log in +00:05 once we exit the application. +00:07 So, we're over hear in the "log_into_account()" +00:09 and the program host, this is super easy. +00:11 We'll just have to get the email from the user, +00:14 like this, so we'll say something to the fact of... +00:18 So, we're going to log in, ask them what their email is +00:19 and let's go ahead and do a "strip()" and a +00:23 ".lower()" on this and in fact, let's always store that. +00:26 So, go back up to our "create_account()" and do that here, +00:29 so ".strip()" takes all the white space +00:31 in case there's like a space or something on the end, +00:33 and ".lower()" of course makes it lowercase. +00:35 So, then we just need to see if the account exists. +00:39 Well, we actually already wrote that so let's say this: +00:43 Say the account is, the service +00:45 not find account by, guessed it, email. +00:49 And then we'll say we had a little error handling +00:51 and say if not account... +00:54 It's an error message. +00:56 So, nothing there, and if it worked +00:58 all we have to do is save it and maybe say, +01:01 "You've logged in, yay!" +01:03 So, let's say "state.active_account = account" +01:09 and then we'll do a success message, "Logged in successfully." +01:15 And then our little prompt will change straight away, +01:18 so that should be good, let's try this. +01:23 Come over here to the host, let's try to log in +01:26 and remember, there's no real passwords we're just +01:29 sort of playing around with accounts here. +01:30 So, michael@talkckpython.fm. +01:35 Boom! "Logged in successfully." +01:37 Awesome, and you can see the prompt change. +01:39 Let's try to log in again and I'll +01:40 try to just use jeff@j.com. +01:42 Nope, "Could not find an email with jeff@j.com." +01:46 Looks like log in is working. diff --git a/video_transcripts/6-conclusion/1-youve-done-it_transcript_final.txt b/video_transcripts/6-conclusion/1-youve-done-it_transcript_final.txt new file mode 100755 index 0000000..9ad2ec5 --- /dev/null +++ b/video_transcripts/6-conclusion/1-youve-done-it_transcript_final.txt @@ -0,0 +1,11 @@ +00:00 There it is, the finish line! +00:01 You have made it, congratulations! +00:04 I hope you've learned a lot throughout this course. +00:06 We really have covered a majority +00:08 of what you need to program against MongoDB. +00:11 So you've done it, you've crossed the line, +00:13 now you know enough to start building applications +00:16 based on MongoDB. +00:17 So the big question is, what are you going to build now? +00:20 I hope you go out and build something amazing. +00:22 Please share it with me when you do. \ No newline at end of file diff --git a/video_transcripts/6-conclusion/2-get-the-code_transcript_final.txt b/video_transcripts/6-conclusion/2-get-the-code_transcript_final.txt new file mode 100755 index 0000000..1bc64d1 --- /dev/null +++ b/video_transcripts/6-conclusion/2-get-the-code_transcript_final.txt @@ -0,0 +1,24 @@ +00:00 I want to take this moment +00:01 to remind you to get the source code, come over to: +00:04 Github.com/MikeCKennedy/MongoDB-QuickStart-Course +00:08 and star and fork this so you're sure to have a copy. +00:12 So one thing I do want to take you through +00:14 really quick, two things. +00:15 One, I covered in the beginning, +00:17 is there is the starter code, which is here. +00:20 This is exactly what we started from. +00:22 And here is what we have finished with. +00:25 You can see, right there, 15 minutes ago +00:27 I wrote the final code. +00:29 But I also wanted to make sure +00:30 there were saved points along the way, +00:32 so if we go back here and check out the branches, +00:35 you'll see there's all these different branches. +00:37 So these are all different save points that you can grab. +00:40 So if I go here, for example, +00:42 you can see final registered and list cages, +00:45 guests can now book a snake into a cage, and things like, +00:48 so these are different points in the course +00:50 that you can go back and forward to. +00:51 So be sure to make use of the branches +00:54 and things like that if that can help you. diff --git a/video_transcripts/6-conclusion/3-full-course_transcript_final.txt b/video_transcripts/6-conclusion/3-full-course_transcript_final.txt new file mode 100755 index 0000000..58e8e34 --- /dev/null +++ b/video_transcripts/6-conclusion/3-full-course_transcript_final.txt @@ -0,0 +1,61 @@ +00:00 You've learned a bunch of stuff +00:01 to program MongoDBs in MongoEngine, +00:04 but there's actually a lot of other things +00:06 that you need to take into consideration +00:08 when you're doing MongoDB in production +00:10 for real applications. +00:11 So I want to encourage you to check out my paid course, +00:15 MongoDB for Python Developers, +00:17 and just some of the things we're covering, +00:19 you've seen a little bit of it, +00:20 but there's actually a lot more. +00:21 So this is over seven hours of professional-grade +00:24 MongoDB and Python programming, +00:26 not just MongoEngine, but the core PyMongo, +00:29 the JavaScript API and so on +00:31 so let's see a little bit what's covered. +00:32 So we talk in depth about how to set up your machine, +00:36 whether it's Windows, Mac, or Linux, +00:38 the tools that we're going to use, +00:39 how to get them installed, +00:41 the theory behind NoSQL, +00:43 why NoSQL, why document databases, +00:45 MongoDB's native shell and native query syntax, +00:48 assuming we run these operations, +00:50 these queries, filter statements and so on in MongoEngine, +00:53 how does that map down to the database? +00:55 This is important +00:56 because when you're running MongoDB in production, +00:59 you need to be able to use the tools +01:01 and the query language to talk to it and manage it, right? +01:04 And that is in this native query API. +01:07 How to model data with documents, +01:09 we did talk about this. +01:10 We go into more depth in this course. +01:13 MongoDB from PyMongo, this is the foundation of MongoEngine +01:17 and basically Python's equivalent +01:19 of the native query syntax. +01:21 MongoEngine, we covered a lot of that in this course. +01:24 This is pretty similar. +01:25 High-performance techniques, +01:26 so performance around document design, +01:28 performance around indexes +01:30 and using profiling to discover where you need those, +01:33 so that's covered in this course. +01:34 And super, super important is how to properly deploy MongoDB +01:38 in production on the internet so it doesn't get hacked +01:41 and you don't lose data or anything like that. +01:44 So if you want to check out this course, +01:45 here's a tremendously long URL +01:47 that you very likely don't want to type so type this, +01:50 bit.ly/mongocourse and that'll take you right there. +01:53 You can check it out. +01:53 I encourage you to take this course. +01:55 If you like what you saw in this course, +01:57 here's seven more hours going even more in depth. +02:00 So with that, I want to say thank you. +02:02 Thank you so much for taking my course. +02:03 I really hope you learned a lot and you enjoyed it. +02:05 Please connect with me on Twitter @mkennedy +02:08 or various other places you find me on the internet. +02:10 Thanks and see you later.