#####################################################
###################### General ######################
#####################################################
print(“Bashar”)

help(print)       # Helps you understand what the function print() does
help(str.rjust)   # Helps you understand what the method rjust() of the function str() does

dir(str)          # Find the available methods and attributes of dir(str)
type('a')         # Gets the type of the passed object 
# OUTPUT: <class 'str'>

# Functions
def square(num):
  return num*num

# Docstring get
func_1.__doc__

# try, except, else
try:
    result = int(arg)
except ValueError:
    print("Invalid int!")
except TypeError:
    print("That's not a string or a number!")
else:
    return result

# While loop
while True:   # Infinite loop using while True
   ...
   break
   ...
   continue

# Random number with randint()
import random
rand_num = random.randint(1, 10)

#####################################################
###################### Strings ######################
#####################################################
print("Hello, " + name + "!")
print("Hello, {}!".format(name) )
# Print a value in the command line

input()
name = input("What is your name? ")
# Takes an input in the command line

format()
my_string1 = "Hi! My name is { } and I live in { }"
my_string1.format('Kenneth', 'Oregon')
my_string2 = "Hi! My name is {name} and I live in {state}"
my_string2.format(name='Kenneth', state='Oregon')
# String formatting, in which non-string values can be passed

strip()
my_string.strip()
# Strip white spaces from the end of a string

"Bashar".rjust()

# Casing
lower() upper() capitalize() title()
my_str.lower()
my_str.upper()
my_str.capitalize()
my_str.title()

rjust(x)
print("Bashar".rjust(8))
# Returns the string right justified in a new string length padded with spaces by default

int(num) , float(num)
# Converts string numbers into actual numbers

del my_str
del my_num
del my_list
del my_list[2]
# Delete variables

#####################################################
####################### Lists #######################
#####################################################

my_list = []
my_list = [ 1, 2.5, 'a' ]
# List creation

len(my_list)
# Length of a list

range(10)
# An iterable from 0 to 9

range(-2,9)
# An iterable from -2 to 8

list(range(10))
# Create a list of numbers from an iterable

list('Bashar')
# [ 'B','a','s','h','a','r' ]
# Create a list of characters from a string (also an iterable)

split()
my_list = "Lorem ipsum dolor sit".split()
# Splits a string by a character into a list

join()
my_string = ' '.join(my_list)
# Joins list items by a character into a string

append()
my_list.append(new_item)
# Appends a list with an item

[1,2,3] + [4,5]
# Append a list with another list

extend()
num_list = [1,2,3]
num_list.extend( range(4,11) )
# Extends a list

insert()
alpha = list('acdf')
alpha.insert(1,'b')
# Inserts item into a list

index()
alphabet.index('z') 
# Returns index of list item

remove()
my_list = [1,2,3,1,False]
my_list.remove(1)
my_list.remove(False)
[2,3,1]
# Removes passed item starting from the beginning of the list

pop()
item_holder = my_list.pop()
# Pops an item from a list into a variable, by default the last list item

insert(x, pop())
the_list.insert(0, the_list.pop(3))
# Move 4th item to the beginning of the list

sort()
num_list.sort()
# Sorts a list according to its type

# Lists iteration
for item in my_list:
   print(item)

# List Slicing [start:stop]
my_string = "Hello there"
my_string[1:5]  #"ello"
my_string[0:5]  #"Hello"
my_string[:5]   #"Hello"
my_string[5:]   #" there"

my_list = [1,2,3,4,5]
my_list[1:3]            #[2,3]
my_list[2:len(my_list)] #[3,4,5]
my_list[:2]             #[1,2]
my_list[:]              #[1,2,3,4,5]
list_copy = my_list[:]  #List copy trick

# Steps [Start:Stop:Step]
my_list = list(range(21))
my_list[::2]            #[0,2,4,6,8, ... ,20]
my_list[2::2]           #[2,4,6,8, ... ,20]
my_list[::-2]           #[20,18,16,14, ... , 2, 0]
my_list[2:8:-1]         #Wrong!
my_list[8:2:-1]         #[8,7,6,5,4,3]
"Oklahoma"[::2]         #'Olhm'
"Oklahoma"[::-1]        #'amohalkO'
"Oklahoma"[-1::-1]      #'amohalkO'

# Slice Control
my_list = [1,2,'a','b','c','d',5,6,7,'f','g','h',8,9,'j']
del my_list[:2]         #['a','b','c','d',5,6,7,'f','g','h',8,9,'j']
my_list[4:7] = ['e','f']#['a','b','c','d','e','f','f','g','h',8,9,'j']
my_list.remove('f')     #['a','b','c','d','e','f','g','h',8,9,'j']
my_list[8:10] = ['i']   #['a','b','c','d','e','f','g','h','i','j']


#####################################################
#################### Dictionaries ###################
#####################################################

# Dictionary 
my_dict = {}
my_dict = {'name': 'Kenneth', 'job': 'Teacher'}
my_dict = {'name': {'last': 'Love', 'first': 'Kenneth'}}
my_dict = {(2,2): True, (1,2): False}

# Keys
my_dict['job']            # 'Teacher'
my_dict['name']['last']   # 'Love'
my_dict[(1,2)]            # False

# Dict Iteration
for key in my_dict:
  if key == item:
    count += 1

# Create & Modify
my_dict                   # {'name': 'Kenneth', 'job': 'Teacher'}
my_dict['age'] = 33       # {'name': 'Kenneth', 'job': 'Teacher', 'age': 33}
my_dict['age'] = 34       # {'name': 'Kenneth', 'job': 'Teacher', 'age': 34}

# Update 
my_dict.update({'job': 'Teacher', 'state': 'Oregon', 'age': '33', 'name': 'Kenneth'})

# Unpacking
my_string = "Hi! My name is {name} and I live in {state}"
my_string.format(**my_dict)

# Iteration Over Keys
for key in my_dict:
  print('{}: {}'.format(key, my_dict[key]))

# Iteration Over Values
for value in my_dict.values():
  print(value)

# Iterate Over items()
for key, value in my_dict.items():
  print('{}: {}'.format(key.title(), value))

# OrderedDict
from collections import OrderedDict
...
OrderedDict([
  ('a', func_a), 
  ('b', func_b), 
])

#####################################################
####################### Tuples ######################
#####################################################

#Tuples 
my_tuple1 = (1,2,3)  # Tuples are immutable lists

# Commas are important not parentheses
my_tuple2 = 1,2,3

# Create Tuple from List
my_tuple3 = tuple([1,2,3])

# Swapping
a, b = 1, 2
a, b = b, a
a     # 2
b     # 1

# Packing
c = (3,4)

# Unpacking 
d, e = c

enumerate()
# Enumerate is a function that iterates through an iterable, using a Tuple of 2 items
for index, letter in enumerate(abc_list):
  print('{}: {}'.format(index, letter))

for step in enumerate(abc_list):           # Whereas step is a packed Tuple
  print('{}: {}'.format(step[0], step[1]))

for step in enumerate(abc_list):           # * for Tuples & Lists, ** for Dictionaries
  print('{}: {}'.format(*step))

#####################################################
######################## Sets #######################
#####################################################

{1, 3, 5}

## Set Characteristics
# 1. Unique
# 2. Comparable
# 3. No Indexes

# Create empty set
var1 = set({})             # var2 = {} # This way you create an empty dictionary

# A trick to eliminate repetition inside a list
my_list = list(set(my_list))


low_primes = {1,3,5,7,11,13}

# Add
low_primes.add(17)         # {1,3,5,7,11,13,17}

# Extend/Update
low_primes.update({19,23}, {2,29})	# {1,2,3,5,7,11,13,17,19,23,29}

# Remove
low_primes.remove(15)      # after adding 15 to the previous set
                           # {1,2,3,5,7,11,13,15,17,19,23,29}

# Removing without errors
low_primes.discard(100)    # {1,2,3,5,7,11,13,17,19,23,29}

# Pop (first item)
low_prime.pop()            # {2,3,5,7,11,13,17,19,23,29}


# Set Functions & Operands
set1 = set(range(10))
set2 = {1,2,3,5,7,11,13,17,19,23}

# Union
set1.union(set2)
set1 | set2
# Output: {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 13, 17, 19, 23}

# Difference
set2.difference(set1)
set2 - set1
# Output: {11, 13, 17, 19, 23}

# Symmetric Difference
set2.symmetric_difference(set1)
set2 ^ set1
# Output: {0, 4, 6, 8, 9, 11, 13, 17, 19, 23}

# Intersection
set1.intersection(set2)
set1 & set2
# Output: {1, 2, 3, 5, 7}


#####################################################
######################## OOP ########################
#####################################################

# Class
class Monster:
  sound = 'roar'    # Attribute

  def battlecry(self):# Method
    return self.sound.upper()

jubjub = Monster()    # Create class instance
jubjub.hit_points = 5 # Assign value to attribute
jubjub.sound          # Access class attribute, OUTPUT: 'roar'
jubjub.battlecry()    # Call class method, OUTPUT:'ROAR'


# Attributes
class Thief:
  sneaky = True

kenneth.sneaky # Calling an instance's attribute
Thief.sneaky # Calling a class's attribute
 
# Methods & Self
class Thief:
  sneaky = True

  def pickpocket(self):
    return bool(random.randint(0,1))

# Unlike attributes, a method can only be used by an instance, not by the actual class. Hence, the use of `self`
# Both of the following statements are equal:
kenneth.pickpocket()
Thief.pickpocket(kenneth)


# __init__
class Monster:
  def __init__(self, hit_points=5, weapon='sword', color='yellow', sound='roar'): # __init__ is the Constructor
    self.hit_points = hit_points
    self.color = color
    self.sound = sound
    # __init__ doesn't need to return anything, it just needs to set the attributes.
    # Remember the order of the arguments!
    
  def battlecry(self):
    return self.sound.upper()

# Class arguments using dicts
class Monster:
  def __init__(self, **kwargs):
    self.hit_points = kwargs.get('hit_points', 1)
    self.color = kwargs.get('color', 'yellow')
    self.sound = kwargs.get('sound', 'roar')

  def battlecry(self):
    return self.sound.upper()

# __str__( )
class Monster: 
  weapon = 'sword'
  sound = 'roar'

  def __str__(self):    # Runs when you print the object
    return "I'm a monster!"

draco = Dragon()
print(draco)      # OUTPUT: "I'm a monster!"


# Adding New Attributes with setattr()
class Monster: 
  weapon = 'sword'
  sound = 'roar'

  def __init__(self, **kwargs):
    for key, value in kwargs.items():
      setattr(self, key, value)

jabberwock = Monster(color='blue', sound='whiffling', hit_points=500, adjective='manxsome') # adjective is new
jabberwock.adjective     # OUTPUT: 'manxsome'

# Inheritence
class Goblin(Monster):  # Parent classes implicitly inherit also from a parent called 'object'
  pass

# Multiple Inheritence
class Thief(Agile, Sneaky, Character):
  pass

# Method Resolution Order (MRO) controls the order that classes are searched through to find a particular method.
# In this case, Character is the final class to be initialized, and Agile's __init__ overrides all.


# Overriding Inheritance
class Combat: 
  attack_limit = 6
  def attack(self):
    # ...
    pass

class Character(Combat):
  attack_limit = 10   # Override
  def attack(self):   # Override
    # ...
    pass


# super()
# Make use of the code living in superclasses.

class Character:
    def __init__(self, name):
        self.name = name

class Thief(Character):
  sneaky = True

    def __init__(self, name):
        super().__init__(name)
        self.sneaky = sneaky


# Python Family Tree Classes

isinstance(<object>, <class>)
issubclass(<class>, <class>)
type(<instance>)


#####################################################
##################### Python DB #####################
#####################################################

# Table = Class/Model
class Student(Model):
  username = CharField(max_length=255, unique=True)
  points = IntegerField(default=0)

  class Meta:
    database = db

# Column = Model Attribute
CharField(), IntegerField(), DateTimeField(), BooleanField(), TextField(), 

#C.R.U.D.
.create()
Student.create( username=name, points=num )
Student.select()

.get()
student_record = Student.get( username = "Bashar" )

.save()
student_record.points = 100
student_record.save()

.select()
student_all = Student.select()
student_top = Student.select().order_by(Student.points.desc()).get()  
student_bashars = Student.select().where(Student.username=="Bashar")
print( "Top student is: {0.username}".format(student_top) )

.delete_instance()
Student.get( username="Bashar" ).delete_instance()

.contains()
entries = Entry.select().where(Entry.content.contains(search_string))
# Searches a query for a string

#STRFTime
entry.timestamp.strftime('%A %B %d, %Y %I: %M %p')

%H:%M:%S    # 22:43:15
%I:%M:%S %p # 10:43:15 PM
%Y-%m-%d    # 2017-09-20
%b          # Sep
%B          # September
%a          # Wed
%A          # Wednesday