├── .gitignore ├── BeautifulSoup ├── requirements.txt ├── scrabble.html ├── scrabble_distribution.py ├── titans_books.html ├── titans_books.py └── wowhead_scraper.py ├── I_still_have_to ├── .gitignore ├── config.py-example └── struggle.py ├── README.md ├── amazon ├── genlink.py └── requirements.txt ├── boss_class_code └── boss_class.py ├── decorator_opt_arg ├── decorators.py ├── decorators_cl.py └── decorators_opt_arg.py ├── django-archive ├── .gitignore ├── archive │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── manage.py ├── requirements.txt └── snippets │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── flask_for_loop ├── main.py ├── requirements.txt ├── static │ └── style.css └── templates │ └── birthdays.html ├── flaskapi ├── app.py ├── curl.py └── test_app.py ├── forks ├── .gitignore ├── commits.py └── requirements.txt ├── generic_emailer └── generic_emailer.py ├── katas ├── course_time │ ├── content.html │ └── js_course_time_scraper.py ├── pypi │ ├── .gitignore │ ├── README.md │ ├── download_feed.py │ ├── pypi100k.py │ └── requirements.txt ├── pypi2 │ └── pypirolls.py └── vowel_count │ └── vowels.py ├── kindle_notes ├── .gitignore ├── kindle_json2html.py └── templates.py ├── notebooks ├── README.md ├── itertools.ipynb ├── pythonic-idioms.ipynb └── tags.txt ├── packt └── packt.py ├── pillow ├── banner │ ├── .gitignore │ ├── __init__.py │ ├── assets │ │ ├── SourceSansPro-Regular.otf │ │ ├── Ubuntu-R.ttf │ │ ├── pillow-logo.png │ │ └── pybites-challenges.png │ └── banner.py └── requirements.txt ├── pybites_digest ├── README.md ├── digest.py └── requirements.txt ├── pybites_review ├── prs.py └── requirements.txt ├── pytest └── fixtures │ ├── .gitignore │ ├── conftest.py │ ├── groceries.py │ ├── requirements.txt │ ├── test_edit_cart.py │ └── test_view_cart.py ├── selenium ├── README.md ├── requirements.txt └── test.py ├── steam_notifier ├── email_list.py ├── emailer.py ├── pull_xml.py ├── readme.txt ├── requirements.txt └── xml_steam_scraper.py ├── tips ├── requirements.txt └── tips.py ├── twitter_bot ├── .gitignore ├── README.md ├── config.py-example ├── feeds-template ├── requirements.txt └── tweetbot.py └── various ├── harrypotter ├── harry.py └── harry.txt ├── mail.py ├── pybites_party.py └── subscribers.py /.gitignore: -------------------------------------------------------------------------------- 1 | **pyc 2 | **swp 3 | **venv 4 | **.cache 5 | **xml 6 | **__pycache__ 7 | **.ipynb_checkpoints 8 | -------------------------------------------------------------------------------- /BeautifulSoup/requirements.txt: -------------------------------------------------------------------------------- 1 | bs4 2 | requests 3 | -------------------------------------------------------------------------------- /BeautifulSoup/scrabble.html: -------------------------------------------------------------------------------- 1 | Scrabble Tile Distribution | Scrabble Wizard
Wooden.Scrabble.Letter.Tiles-all

Scrabble Tile Distribution

Tile count and value ordered by letter:

TileCountValue
A91
B23
C23
D42
E121
F24
G32
H24
I91
J18
K15
L41
M23
N61
O81
P23
Q110
R61
S41
T61
U41
V24
W24
X18
Y24
Z110

Blank

2

0

Tile count and value ordered by count

TileCountValue
E121
A91
I91
O81
N61
R61
T61
L41
S41
U41
D42
G32
Blank20
B23
C23
M23
P23
F24
H24
V24
W24
Y24
K15
J18
X18
Q110
Z110
-------------------------------------------------------------------------------- /BeautifulSoup/scrabble_distribution.py: -------------------------------------------------------------------------------- 1 | #!python3 2 | # scrape site with BeautifulSoup to get srabble distribution 3 | 4 | from collections import namedtuple 5 | import os 6 | from string import ascii_uppercase 7 | 8 | from bs4 import BeautifulSoup as Soup 9 | import requests 10 | 11 | URL = 'http://scrabblewizard.com/scrabble-tile-distribution/' 12 | HTML = 'scrabble.html' 13 | LETTERS = list(ascii_uppercase) # exclude 2x blanks (scrabble wildcards) 14 | LETTER_REPR = 'Letter: {} - amount: {} / value: {}' 15 | Letter = namedtuple('Letter', 'name amount value') 16 | 17 | 18 | def get_html(): 19 | """Retrieve html from cache or URL""" 20 | if os.path.isfile(HTML): 21 | with open(HTML) as f: 22 | return f.read() 23 | return requests.get(URL).text 24 | 25 | 26 | def get_table_rows(html): 27 | """Parse scrabble tile distribution into data structure 28 | Even lack of CSS selectors can be worked around :) 29 | Thanks SO - 23377533/python-beautifulsoup-parsing-table""" 30 | soup = Soup(html, 'html.parser') 31 | table = soup.find('table') 32 | table_body = table.find('tbody') 33 | return table_body.find_all('tr') 34 | 35 | 36 | def get_distribution(rows): 37 | """Parse the table rows and convert them in a list of named tuples""" 38 | for row in rows: 39 | cols = row.find_all('td') 40 | cols = [ele.text.strip() for ele in cols] 41 | if cols[0] not in LETTERS: 42 | continue 43 | yield Letter(*cols) 44 | 45 | 46 | if __name__ == "__main__": 47 | html = get_html() 48 | rows = get_table_rows(html) 49 | distribution = list(get_distribution(rows)) 50 | total_amount = sum(int(letter.amount) for letter in distribution) 51 | 52 | assert total_amount == 98 # 100 - 2 blanks 53 | 54 | for letter in distribution: 55 | print(LETTER_REPR.format(*letter)) 56 | -------------------------------------------------------------------------------- /BeautifulSoup/titans_books.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | from itertools import dropwhile 3 | import os 4 | from pprint import pprint as pp 5 | import re 6 | 7 | from bs4 import BeautifulSoup as Soup 8 | import requests 9 | 10 | AMAZON = "amazon.com" 11 | CACHED_HTML = "titans_books.html" 12 | SHORT_SRC = "http://bit.ly/2gP0fv3" 13 | TITLE = re.compile(r'.*{}/([^/]+).*'.format(AMAZON)).sub 14 | 15 | def get_html(): 16 | if os.path.isfile(CACHED_HTML): 17 | with open(CACHED_HTML) as f: 18 | html = f.read().lower() 19 | else: 20 | html = requests.get(SHORT_SRC).text 21 | return Soup(html) 22 | 23 | def get_books(): 24 | cnt = Counter() 25 | for a in get_html().find_all('a', href=True): 26 | href = a['href'] 27 | if AMAZON in href: 28 | book = TITLE(r'\1', href) 29 | cnt[book] += 1 30 | return cnt 31 | 32 | 33 | def get_multiple_mentions(books, keep=2): 34 | for key, count in dropwhile(lambda key_count: key_count[1] >= keep, books.most_common()): 35 | del books[key] 36 | return books 37 | 38 | 39 | def print_results(books): 40 | for book, count in books.items(): 41 | print("{:<3} {}".format(count, book)) 42 | 43 | 44 | if __name__ == "__main__": 45 | def test(cnt): 46 | assert(cnt["tao-te-ching-laozi"] == 3) 47 | assert(cnt["influence-psychology-persuasion-robert-cialdini"] == 2) 48 | assert(sum(cnt.values()) == 204) 49 | 50 | books = get_books() 51 | pp(books) 52 | test(books) 53 | multiple_mentions = get_multiple_mentions(books) 54 | print_results(multiple_mentions) 55 | -------------------------------------------------------------------------------- /BeautifulSoup/wowhead_scraper.py: -------------------------------------------------------------------------------- 1 | #!python3 2 | # wowhead_scraper.py is a simple web scraper to check for the latest headlines 3 | # on Wowhead. 4 | 5 | import requests 6 | import bs4 7 | 8 | # URL of site we want to scrape 9 | URL = "http://www.wowhead.com" 10 | header_list = [] 11 | 12 | def main(): 13 | raw_site_page = requests.get(URL) #Pull down the site. 14 | raw_site_page.raise_for_status() #Confirm site was pulled. Error if not 15 | 16 | #Create BeautifulSoup object 17 | soup = bs4.BeautifulSoup(raw_site_page.text, 'html.parser') 18 | html_header_list = soup.select('.heading-size-1') 19 | for headers in html_header_list: 20 | header_list.append(headers.getText()) 21 | for headers in header_list: 22 | print(headers) 23 | 24 | if __name__ == "__main__": 25 | main() 26 | -------------------------------------------------------------------------------- /I_still_have_to/.gitignore: -------------------------------------------------------------------------------- 1 | config.py 2 | data* 3 | trend.py 4 | -------------------------------------------------------------------------------- /I_still_have_to/config.py-example: -------------------------------------------------------------------------------- 1 | LOGFILE = "bot.log" 2 | HASHTAG = '#python' 3 | 4 | # twitter api 5 | CONSUMER_KEY = '' 6 | CONSUMER_SECRET = '' 7 | ACCESS_TOKEN = '' 8 | ACCESS_SECRET = '' 9 | -------------------------------------------------------------------------------- /I_still_have_to/struggle.py: -------------------------------------------------------------------------------- 1 | from pprint import pprint as pp 2 | import sys 3 | import time 4 | 5 | import tweepy 6 | 7 | from config import CONSUMER_KEY, CONSUMER_SECRET 8 | from config import ACCESS_TOKEN, ACCESS_SECRET 9 | 10 | NOW = int(time.time()) 11 | SEARCH = 'my name is years python' 12 | 13 | auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET) 14 | auth.set_access_token(ACCESS_TOKEN, ACCESS_SECRET) 15 | 16 | api = tweepy.API(auth) 17 | 18 | 19 | def get_tweets(search): 20 | for tweet in tweepy.Cursor(api.search, 21 | q=search, 22 | rpp=100, 23 | result_type="recent", 24 | include_entities=True, 25 | lang="en").items(): 26 | if not tweet.retweeted and 'RT @' not in tweet.text: 27 | yield tweet 28 | 29 | 30 | if __name__ == "__main__": 31 | if len(sys.argv) > 1: 32 | search = ' '.join(sys.argv[1:]) 33 | else: 34 | search = SEARCH 35 | 36 | outfile = 'data_{}.txt'.format(NOW) 37 | with open(outfile, 'w') as f: 38 | for tweet in get_tweets(search): 39 | pp(tweet) 40 | f.write('{}\n'.format(tweet.text)) 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## PyBites Blog Code 2 | 3 | Sample code for our blog [http://pybit.es](http://pybit.es). 4 | -------------------------------------------------------------------------------- /amazon/genlink.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import re 3 | import sys 4 | 5 | import pyperclip 6 | 7 | AMAZON = 'amazon' 8 | CODE = 'pyb0f-20' 9 | LINK = 'http://www.amazon.com/dp/{}/?tag={}' 10 | # http://pybit.es/mastering-regex.html 11 | URL = re.compile(r""" 12 | ^https://(?:www.)?amazon.com/ 13 | [^/]+/ 14 | dp/ 15 | (?P[^/]+) # the numberic asin follows the dp/ 16 | /ref=.*""", re.VERBOSE) 17 | 18 | # http://pybit.es/pyperclip.html 19 | url = pyperclip.paste() 20 | 21 | if AMAZON not in url or '/dp/' not in url: 22 | sys.exit('Copy URL and run this script again') 23 | 24 | print('Grabbed link from clipboard: \n'.format(url)) 25 | 26 | m = URL.match(url) 27 | if not m: 28 | sys.exit('URL does not match') 29 | 30 | asin = m.group('asin') 31 | link = LINK.format(asin, CODE) 32 | 33 | pyperclip.copy(link) 34 | print('Copied link to clipboard: \n{}'.format(link)) 35 | -------------------------------------------------------------------------------- /amazon/requirements.txt: -------------------------------------------------------------------------------- 1 | pyperclip 2 | -------------------------------------------------------------------------------- /boss_class_code/boss_class.py: -------------------------------------------------------------------------------- 1 | #!python3 2 | #boss_class.py is a script to demo Python Classes and Subclasses 3 | 4 | class Boss(object): 5 | def __init__(self, name, attitude, behaviour, face): 6 | self.name = name 7 | self.attitude = attitude 8 | self.behaviour = behaviour 9 | self.face = face 10 | 11 | def get_attitude(self): 12 | return self.attitude 13 | 14 | def get_behaviour(self): 15 | return self.behaviour 16 | 17 | def get_face(self): 18 | return self.face 19 | 20 | class GoodBoss(Boss): 21 | def __init__(self, 22 | name, 23 | attitude, 24 | behaviour, 25 | face): 26 | super().__init__(name, attitude, behaviour, face) 27 | 28 | def nurture_talent(self): 29 | #A good boss nurtures talent making employees happy! 30 | print("The employees feel all warm and fuzzy then put their talents to good use.") 31 | 32 | def encourage(self): 33 | #A good boss encourages their employees! 34 | print("The team cheers, starts shouting awesome slogans then gets back to work.") 35 | 36 | 37 | class BadBoss(Boss): 38 | def __init__(self, 39 | name, 40 | attitude, 41 | behaviour, 42 | face): 43 | super().__init__(name, attitude, behaviour, face) 44 | 45 | def hoard_praise(self): 46 | #A bad boss takes all the praise for him/herself 47 | print("The employees feel cheated and start plotting {}'s demise while he stares at his own reflection.".format(self.name)) 48 | 49 | def yell(self): 50 | #A bad boss yells! (Ain't nobody got time for that!) 51 | print("Everyone stares while {} yells. Someone shouts, 'Won't somebody PLEASE think of the children!'".format(self.name)) 52 | print("{} storms off, everyone comforts the victim and one person offers to arrange an 'accident' for {}.".format(self.name, self.name)) -------------------------------------------------------------------------------- /decorator_opt_arg/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | import time 3 | 4 | def sleep(seconds=None): 5 | def real_decorator(func): 6 | @wraps(func) 7 | def wrapper(*args, **kwargs): 8 | print('Sleeping for {} seconds'.format(seconds)) 9 | time.sleep(seconds if seconds else 1) 10 | return func(*args, **kwargs) 11 | return wrapper 12 | return real_decorator 13 | 14 | 15 | if __name__ == '__main__': 16 | 17 | @sleep(1) # @sleep without arg fails 18 | def hello(): 19 | print('hello world') 20 | 21 | for _ in range(3): 22 | hello() 23 | -------------------------------------------------------------------------------- /decorator_opt_arg/decorators_cl.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | import time 3 | 4 | class sleep: 5 | 6 | def __init__(self, seconds=None): 7 | self.seconds = seconds if seconds else 1 8 | 9 | def __call__(self, func): 10 | wraps(func)(self) 11 | def wrapped_f(*args): 12 | print('Sleeping for {} seconds'.format(self.seconds)) 13 | time.sleep(self.seconds) 14 | func(*args) 15 | return wrapped_f 16 | 17 | 18 | if __name__ == '__main__': 19 | 20 | @sleep(1) # @sleep without arg fails 21 | def hello(): 22 | print('hello world') 23 | 24 | for _ in range(3): 25 | hello() 26 | -------------------------------------------------------------------------------- /decorator_opt_arg/decorators_opt_arg.py: -------------------------------------------------------------------------------- 1 | from functools import wraps, partial 2 | import time 3 | 4 | def sleep(func=None, *, seconds=None, msg=None): 5 | if func is None: 6 | return partial(sleep, seconds=seconds, msg=msg) 7 | 8 | seconds = seconds if seconds else 1 9 | msg = msg if msg else 'Sleeping for {} seconds'.format(seconds) 10 | 11 | @wraps(func) 12 | def wrapper(*args, **kwargs): 13 | print(msg) 14 | time.sleep(seconds) 15 | return func(*args, **kwargs) 16 | return wrapper 17 | 18 | 19 | if __name__ == '__main__': 20 | 21 | def call_n_times(func, n=3): 22 | for _ in range(n): 23 | func() 24 | 25 | @sleep # works now! 26 | def hello(): 27 | print('hello world') 28 | 29 | print('\nWithout args\n---') 30 | call_n_times(hello) 31 | 32 | 33 | @sleep(seconds=2) 34 | def hello(): 35 | print('hello world') 36 | 37 | print('\nWith one opt arg: seconds\n---') 38 | call_n_times(hello) 39 | 40 | 41 | @sleep(seconds=1, msg='I work so hard, resting a bit') 42 | def hello(): 43 | print('hello world') 44 | 45 | print('\nWith two opt args: seconds and msg\n---') 46 | call_n_times(hello) 47 | -------------------------------------------------------------------------------- /django-archive/.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | db.sqlite3 3 | -------------------------------------------------------------------------------- /django-archive/archive/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybites/blog_code/902ebb87e5f7a407714d0e399833f0331a1b915d/django-archive/archive/__init__.py -------------------------------------------------------------------------------- /django-archive/archive/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Script 4 | 5 | 6 | class ScriptAdmin(admin.ModelAdmin): 7 | pass 8 | admin.site.register(Script, ScriptAdmin) 9 | -------------------------------------------------------------------------------- /django-archive/archive/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ArchiveConfig(AppConfig): 5 | name = 'archive' 6 | -------------------------------------------------------------------------------- /django-archive/archive/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2019-05-08 02:14 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Script', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=100)), 19 | ('code', models.TextField()), 20 | ('added', models.DateTimeField(auto_now_add=True)), 21 | ], 22 | options={ 23 | 'ordering': ['-added'], 24 | }, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /django-archive/archive/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybites/blog_code/902ebb87e5f7a407714d0e399833f0331a1b915d/django-archive/archive/migrations/__init__.py -------------------------------------------------------------------------------- /django-archive/archive/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Script(models.Model): 5 | name = models.CharField(max_length=100) 6 | code = models.TextField() 7 | added = models.DateTimeField(auto_now_add=True) 8 | 9 | def __str__(self): 10 | return self.name 11 | 12 | class Meta: 13 | ordering = ['-added'] 14 | -------------------------------------------------------------------------------- /django-archive/archive/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /django-archive/archive/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | app_name = 'archive' 6 | urlpatterns = [ 7 | path('download/', views.download, name='download') 8 | ] 9 | -------------------------------------------------------------------------------- /django-archive/archive/views.py: -------------------------------------------------------------------------------- 1 | import zipfile 2 | 3 | from django.http import HttpResponse 4 | 5 | from .models import Script 6 | 7 | README_NAME = 'README.md' 8 | README_CONTENT = """ 9 | ## PyBites Code Snippet Archive 10 | 11 | Here is a zipfile with some useful code snippets. 12 | 13 | Produced for blog post https://pybit.es/django-zipfiles.html 14 | 15 | Keep calm and code in Python! 16 | """ 17 | ZIPFILE_NAME = 'pybites_codesnippets.zip' 18 | 19 | 20 | def download(request): 21 | """Download archive zip file of code snippets""" 22 | response = HttpResponse(content_type='application/zip') 23 | zf = zipfile.ZipFile(response, 'w') 24 | 25 | # create the zipfile in memory using writestr 26 | # add a readme 27 | zf.writestr(README_NAME, README_CONTENT) 28 | 29 | # retrieve snippets from ORM and them to zipfile 30 | scripts = Script.objects.all() 31 | for snippet in scripts: 32 | zf.writestr(snippet.name, snippet.code) 33 | 34 | # return as zipfile 35 | response['Content-Disposition'] = f'attachment; filename={ZIPFILE_NAME}' 36 | return response 37 | -------------------------------------------------------------------------------- /django-archive/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'snippets.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /django-archive/requirements.txt: -------------------------------------------------------------------------------- 1 | django-2.2 2 | -------------------------------------------------------------------------------- /django-archive/snippets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybites/blog_code/902ebb87e5f7a407714d0e399833f0331a1b915d/django-archive/snippets/__init__.py -------------------------------------------------------------------------------- /django-archive/snippets/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for snippets project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = os.environ['SECRET_KEY'] 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'archive', 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'snippets.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'snippets.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 106 | 107 | LANGUAGE_CODE = 'en-us' 108 | 109 | TIME_ZONE = 'UTC' 110 | 111 | USE_I18N = True 112 | 113 | USE_L10N = True 114 | 115 | USE_TZ = True 116 | 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 120 | 121 | STATIC_URL = '/static/' 122 | -------------------------------------------------------------------------------- /django-archive/snippets/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | 4 | urlpatterns = [ 5 | path('admin/', admin.site.urls), 6 | path('', include('archive.urls', namespace='archive')), 7 | ] 8 | -------------------------------------------------------------------------------- /django-archive/snippets/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for snippets project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'snippets.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /flask_for_loop/main.py: -------------------------------------------------------------------------------- 1 | #!python3 2 | 3 | from flask import Flask, render_template 4 | 5 | app = Flask(__name__) 6 | 7 | @app.route("/birthdays") 8 | def birthdays(): 9 | dates = {"Julian": 25, "Bob": 26, "Dan": 47, "Cornelius": 3} 10 | return render_template("birthdays.html", dates=dates) 11 | 12 | if __name__ == "__main__": 13 | app.run() 14 | -------------------------------------------------------------------------------- /flask_for_loop/requirements.txt: -------------------------------------------------------------------------------- 1 | click 2 | Flask 3 | -------------------------------------------------------------------------------- /flask_for_loop/static/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | text-align: center; 3 | } 4 | 5 | .centered { 6 | margin: auto; 7 | width: 20%; 8 | } 9 | 10 | .thick-border { 11 | border: 3px solid black; 12 | border-collapse: collapse; 13 | } 14 | 15 | th, td { 16 | border: 1px solid black; 17 | border-collapse: collapse; 18 | } -------------------------------------------------------------------------------- /flask_for_loop/templates/birthdays.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% for k, v in dates.items() %} 11 | 12 | 13 | 14 | 15 | {% endfor %} 16 |
First nameAge
{{ k }}{{ v }}
-------------------------------------------------------------------------------- /flaskapi/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, abort, make_response, request 2 | 3 | NOT_FOUND = 'Not found' 4 | BAD_REQUEST = 'Bad request' 5 | 6 | app = Flask(__name__) 7 | 8 | items = [ 9 | { 10 | 'id': 1, 11 | 'name': 'laptop', 12 | 'value': 1000 13 | }, 14 | { 15 | 'id': 2, 16 | 'name': 'chair', 17 | 'value': 300, 18 | }, 19 | { 20 | 'id': 3, 21 | 'name': 'book', 22 | 'value': 20, 23 | }, 24 | ] 25 | 26 | 27 | def _get_item(id): 28 | return [item for item in items if item['id'] == id] 29 | 30 | 31 | def _record_exists(name): 32 | return [item for item in items if item["name"] == name] 33 | 34 | 35 | @app.errorhandler(404) 36 | def not_found(error): 37 | return make_response(jsonify({'error': NOT_FOUND}), 404) 38 | 39 | 40 | @app.errorhandler(400) 41 | def bad_request(error): 42 | return make_response(jsonify({'error': BAD_REQUEST}), 400) 43 | 44 | 45 | @app.route('/api/v1.0/items', methods=['GET']) 46 | def get_items(): 47 | return jsonify({'items': items}) 48 | 49 | 50 | @app.route('/api/v1.0/items/', methods=['GET']) 51 | def get_item(id): 52 | item = _get_item(id) 53 | if not item: 54 | abort(404) 55 | return jsonify({'items': item}) 56 | 57 | 58 | @app.route('/api/v1.0/items', methods=['POST']) 59 | def create_item(): 60 | if not request.json or 'name' not in request.json or 'value' not in request.json: 61 | abort(400) 62 | item_id = items[-1].get("id") + 1 63 | name = request.json.get('name') 64 | if _record_exists(name): 65 | abort(400) 66 | value = request.json.get('value') 67 | if type(value) is not int: 68 | abort(400) 69 | item = {"id": item_id, "name": name, 70 | "value": value} 71 | items.append(item) 72 | return jsonify({'item': item}), 201 73 | 74 | 75 | @app.route('/api/v1.0/items/', methods=['PUT']) 76 | def update_item(id): 77 | item = _get_item(id) 78 | if len(item) == 0: 79 | abort(404) 80 | if not request.json: 81 | abort(400) 82 | name = request.json.get('name', item[0]['name']) 83 | value = request.json.get('value', item[0]['value']) 84 | if type(value) is not int: 85 | abort(400) 86 | item[0]['name'] = name 87 | item[0]['value'] = value 88 | return jsonify({'item': item[0]}), 200 89 | 90 | 91 | @app.route('/api/v1.0/items/', methods=['DELETE']) 92 | def delete_item(id): 93 | item = _get_item(id) 94 | if len(item) == 0: 95 | abort(404) 96 | items.remove(item[0]) 97 | return jsonify({}), 204 98 | 99 | 100 | if __name__ == '__main__': 101 | app.run(debug=True) 102 | -------------------------------------------------------------------------------- /flaskapi/curl.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | tests = """ 4 | # get items 5 | curl -i http://127.0.0.1:5000/api/v1.0/items 6 | # item 1 7 | curl -i http://127.0.0.1:5000/api/v1.0/items/1 8 | # item2 9 | curl -i http://127.0.0.1:5000/api/v1.0/items/2 10 | # err: non-existing item 11 | curl -i http://127.0.0.1:5000/api/v1.0/items/4 12 | # err: add item already in items 13 | curl -i -H "Content-Type: application/json" -X POST -d '{"name":"book", "value": 20}' http://127.0.0.1:5000/api/v1.0/items 14 | # err: add item with value not int 15 | curl -i -H "Content-Type: application/json" -X POST -d '{"name":"monitor", "value": "200"}' http://127.0.0.1:5000/api/v1.0/items 16 | # add item with proper values 17 | curl -i -H "Content-Type: application/json" -X POST -d '{"name":"monitor", "value": 200}' http://127.0.0.1:5000/api/v1.0/items 18 | # show items 19 | curl -i http://127.0.0.1:5000/api/v1.0/items 20 | # err: edit non-existing item 21 | curl -i -H "Content-Type: application/json" -X PUT -d '{"value": 30}' http://127.0.0.1:5000/api/v1.0/items/5 22 | # OK: edit existing item 23 | curl -i -H "Content-Type: application/json" -X PUT -d '{"value": 30}' http://127.0.0.1:5000/api/v1.0/items/3 24 | # show items 25 | curl -i http://127.0.0.1:5000/api/v1.0/items 26 | # err: delete non-existing item 27 | curl -i -H "Content-Type: application/json" -X DELETE http://127.0.0.1:5000/api/v1.0/items/5 28 | # OK: delete existing item 29 | curl -i -H "Content-Type: application/json" -X DELETE http://127.0.0.1:5000/api/v1.0/items/3 30 | # show items 31 | curl -i http://127.0.0.1:5000/api/v1.0/items 32 | """ 33 | 34 | for line in tests.strip().split('\n'): 35 | print('\n{}'.format(line)) 36 | if not line.startswith('#'): 37 | cmd = line.strip() 38 | os.system(cmd) 39 | -------------------------------------------------------------------------------- /flaskapi/test_app.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | import unittest 3 | import json 4 | 5 | import app 6 | 7 | BASE_URL = 'http://127.0.0.1:5000/api/v1.0/items' 8 | BAD_ITEM_URL = '{}/5'.format(BASE_URL) 9 | GOOD_ITEM_URL = '{}/3'.format(BASE_URL) 10 | 11 | 12 | class TestFlaskApi(unittest.TestCase): 13 | 14 | def setUp(self): 15 | self.backup_items = deepcopy(app.items) # no references! 16 | self.app = app.app.test_client() 17 | self.app.testing = True 18 | 19 | def test_get_all(self): 20 | response = self.app.get(BASE_URL) 21 | data = json.loads(response.get_data()) 22 | self.assertEqual(response.status_code, 200) 23 | self.assertEqual(len(data['items']), 3) 24 | 25 | def test_get_one(self): 26 | response = self.app.get(BASE_URL) 27 | data = json.loads(response.get_data()) 28 | self.assertEqual(response.status_code, 200) 29 | self.assertEqual(data['items'][0]['name'], 'laptop') 30 | 31 | def test_item_not_exist(self): 32 | response = self.app.get(BAD_ITEM_URL) 33 | self.assertEqual(response.status_code, 404) 34 | 35 | def test_post(self): 36 | # missing value field = bad 37 | item = {"name": "some_item"} 38 | response = self.app.post(BASE_URL, 39 | data=json.dumps(item), 40 | content_type='application/json') 41 | self.assertEqual(response.status_code, 400) 42 | # value field cannot take str 43 | item = {"name": "screen", "value": 'string'} 44 | response = self.app.post(BASE_URL, 45 | data=json.dumps(item), 46 | content_type='application/json') 47 | self.assertEqual(response.status_code, 400) 48 | # valid: both required fields, value takes int 49 | item = {"name": "screen", "value": 200} 50 | response = self.app.post(BASE_URL, 51 | data=json.dumps(item), 52 | content_type='application/json') 53 | self.assertEqual(response.status_code, 201) 54 | data = json.loads(response.get_data()) 55 | self.assertEqual(data['item']['id'], 4) 56 | self.assertEqual(data['item']['name'], 'screen') 57 | # cannot add item with same name again 58 | item = {"name": "screen", "value": 200} 59 | response = self.app.post(BASE_URL, 60 | data=json.dumps(item), 61 | content_type='application/json') 62 | self.assertEqual(response.status_code, 400) 63 | 64 | def test_update(self): 65 | item = {"value": 30} 66 | response = self.app.put(GOOD_ITEM_URL, 67 | data=json.dumps(item), 68 | content_type='application/json') 69 | self.assertEqual(response.status_code, 200) 70 | data = json.loads(response.get_data()) 71 | self.assertEqual(data['item']['value'], 30) 72 | # proof need for deepcopy in setUp: update app.items should not affect self.backup_items 73 | # this fails when you use shallow copy 74 | self.assertEqual(self.backup_items[2]['value'], 20) # org value 75 | 76 | def test_update_error(self): 77 | # cannot edit non-existing item 78 | item = {"value": 30} 79 | response = self.app.put(BAD_ITEM_URL, 80 | data=json.dumps(item), 81 | content_type='application/json') 82 | self.assertEqual(response.status_code, 404) 83 | # value field cannot take str 84 | item = {"value": 'string'} 85 | response = self.app.put(GOOD_ITEM_URL, 86 | data=json.dumps(item), 87 | content_type='application/json') 88 | self.assertEqual(response.status_code, 400) 89 | 90 | def test_delete(self): 91 | response = self.app.delete(GOOD_ITEM_URL) 92 | self.assertEqual(response.status_code, 204) 93 | response = self.app.delete(BAD_ITEM_URL) 94 | self.assertEqual(response.status_code, 404) 95 | 96 | def tearDown(self): 97 | # reset app.items to initial state 98 | app.items = self.backup_items 99 | 100 | 101 | if __name__ == "__main__": 102 | unittest.main() 103 | -------------------------------------------------------------------------------- /forks/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite 2 | -------------------------------------------------------------------------------- /forks/commits.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | import datetime 3 | import time 4 | 5 | import requests 6 | import requests_cache 7 | 8 | USER = 'pybites' 9 | REPO = 'challenges' 10 | BASE_URL = 'https://api.github.com/repos/{}/{}'.format(USER, REPO) 11 | FORK_URL = '{}/forks?page='.format(BASE_URL) 12 | HOUR_IN_SECONDS = 60 * 60 13 | 14 | requests_cache.install_cache('cache', backend='sqlite', expire_after=HOUR_IN_SECONDS) 15 | 16 | Fork = namedtuple('Fork', 'url updated pushed') 17 | 18 | def get_utstamp(tstamp): 19 | dt = datetime.datetime.strptime(tstamp, "%Y-%m-%dT%H:%M:%SZ") 20 | return int(time.mktime(dt.timetuple())) 21 | 22 | 23 | def last_change(f): 24 | updated = get_utstamp(f.updated) 25 | pushed = get_utstamp(f.pushed) 26 | return max(updated, pushed) 27 | 28 | 29 | def get_forks(): 30 | page_num = 0 31 | while True: 32 | page_num += 1 33 | url = FORK_URL + str(page_num) 34 | response = requests.get(url) 35 | # print('getting data for url {}'.format(url)) 36 | d = response.json() 37 | if not d: 38 | return 39 | for row in d: 40 | url = row['html_url'] 41 | updated = row['updated_at'] 42 | pushed = row['pushed_at'] 43 | yield Fork(url, updated, pushed) 44 | 45 | 46 | if __name__ == "__main__": 47 | forks = {} 48 | for fork in get_forks(): 49 | forks[fork.url] = fork 50 | 51 | fmt = '{:<60} | {:<20} | {:<20}' 52 | header = fmt.format(*Fork._fields) 53 | print(header) 54 | for fork in sorted(forks.values(), key=lambda x: last_change(x), reverse=True): 55 | entry = fmt.format(*fork) 56 | print(entry) 57 | print('Total forks: {}'.format(len(forks))) 58 | -------------------------------------------------------------------------------- /forks/requirements.txt: -------------------------------------------------------------------------------- 1 | httpretty 2 | requests 3 | requests-cache 4 | -------------------------------------------------------------------------------- /generic_emailer/generic_emailer.py: -------------------------------------------------------------------------------- 1 | #!python3 2 | #emailer.py is a simple script for sending emails using smtplib 3 | #The idea is to assign a web-scraped file to the DATA_FILE constant. 4 | #The data in the file is then read in and sent as the body of the email. 5 | 6 | import smtplib 7 | from email.mime.multipart import MIMEMultipart 8 | from email.mime.text import MIMEText 9 | 10 | from email_list import EMAILS 11 | 12 | DATA_FILE = 'scraped_data_file' 13 | from_addr = 'your_email@gmail.com' 14 | to_addr = 'your_email@gmail.com' #Or any generic email you want all recipients to see 15 | bcc = EMAILS 16 | 17 | msg = MIMEMultipart() 18 | msg['From'] = from_addr 19 | msg['To'] = to_addr 20 | msg['Subject'] = 'Subject Line' 21 | 22 | with open(DATA_FILE) as f: 23 | body = f.read() 24 | 25 | msg.attach(MIMEText(body, 'plain')) 26 | 27 | smtp_server = smtplib.SMTP('smtp.gmail.com', 587) #Specify Gmail Mail server 28 | 29 | smtp_server.ehlo() #Send mandatory 'hello' message to SMTP server 30 | 31 | smtp_server.starttls() #Start TLS Encryption as we're not using SSL. 32 | 33 | #Login to gmail: Account | Password 34 | smtp_server.login(' your_email@gmail.com ', ' GMAIL APPLICATION ID ') 35 | 36 | text = msg.as_string() 37 | 38 | #Compile email list: From, To, Email body 39 | smtp_server.sendmail(from_addr, [to_addr] + bcc, text) 40 | 41 | #Close connection to SMTP server 42 | smtp_server.quit() 43 | 44 | #Test Message to verify all passes 45 | print('Email sent successfully') 46 | -------------------------------------------------------------------------------- /katas/course_time/content.html: -------------------------------------------------------------------------------- 1 | Class Curriculum 2 | 3 | Introduction 4 | 5 | Start What is Practical JavaScript? (3:47) 6 | 7 | Start The voice in your ear (4:41) 8 | 9 | Start Is this course right for you? (1:21) 10 | 11 | Start What you'll build (5:32) 12 | 13 | Start The development process (2:23) 14 | Support 15 | 16 | Start Getting help 17 | 18 | Start Live office hours every week 19 | Tools 20 | 21 | Start Get Google Chrome (1:01) 22 | 23 | Start Get Plunker (0:43) 24 | Version 1 - Arrays 25 | 26 | Start Requirements (1:46) 27 | 28 | Start It should have a place to store todos (4:08) 29 | 30 | Start It should have a way to display todos (3:20) 31 | 32 | Start It should have a way to add new todos (2:17) 33 | 34 | Start It should have a way to change a todo (4:03) 35 | 36 | Start It should have a way to delete a todo (2:48) 37 | 38 | Start Review (3:04) 39 | Version 2 - Functions 40 | 41 | Start Functions are just recipes (3:36) 42 | 43 | Start Customizing functions (6:45) 44 | 45 | Start Requirements (2:12) 46 | 47 | Start It should have a function to display todos (3:54) 48 | 49 | Start It should have a function to add new todos (5:41) 50 | 51 | Start It should have a function to change a todo (6:08) 52 | 53 | Start It should have a function to delete a todo (3:01) 54 | 55 | Start Review (4:15) 56 | Version 3 - Objects 57 | 58 | Start What is an object? (5:30) 59 | 60 | Start Objects and functions (4:46) 61 | 62 | Start Using Plunker (2:49) 63 | 64 | Start Requirements (0:52) 65 | 66 | Start It should store the todos array on an object (3:31) 67 | 68 | Start It should have a displayTodos method (3:11) 69 | 70 | Start It should have an addTodo method (3:13) 71 | 72 | Start It should have a changeTodo method (3:10) 73 | 74 | Start It should have a deleteTodo method (3:28) 75 | 76 | Start Review (1:15) 77 | Version 4 - Booleans 78 | 79 | Start Requirements (1:10) 80 | 81 | Start todoList.addTodo should add objects (5:14) 82 | 83 | Start todoList.changeTodo should change the todoText property (4:27) 84 | 85 | Start todoList.toggleCompleted should flip the completed property (6:06) 86 | 87 | Start Review (1:39) 88 | Version 5 - Loops of Logic 89 | 90 | Start The for loop (7:01) 91 | 92 | Start Looping over arrays (4:26) 93 | 94 | Start Requirements (0:40) 95 | 96 | Start displayTodos should show .todoText (6:36) 97 | 98 | Start displayTodos should tell you if .todos is empty (6:58) 99 | 100 | Start displayTodos should show .completed (6:11) 101 | 102 | Start Review (1:40) 103 | Version 6 - Thinking in Code 104 | 105 | Start Requirements (3:05) 106 | 107 | Start .toggleAll: If everything's true, make everything false (6:44) 108 | 109 | Start .toggleAll: Otherwise, make everything true (4:02) 110 | 111 | Start Review (2:05) 112 | Interlude - Data types and comparisons 113 | 114 | Start Data types overview (3:36) 115 | 116 | Start Comparisons with primitives (2:51) 117 | 118 | Start Comparisons with objects (7:16) 119 | 120 | Start Review (2:56) 121 | Version 7 - HTML and the DOM 122 | 123 | Start Requirements (1:28) 124 | 125 | Start HTML essentials (8:21) 126 | 127 | Start What's the DOM? (6:44) 128 | 129 | Start There should be a Display todos button and a Toggle all button in the app (2:36) 130 | 131 | Start Clicking Display todos should run todoList.displayTodos (9:48) 132 | 133 | Start Clicking Toggle all should run todoList.toggleAll (3:34) 134 | 135 | Start Review (2:25) 136 | Interlude - Don't wonder about things the debugger can tell you 137 | 138 | Start todoList.displayTodos (5:27) 139 | 140 | Start todoList.addTodo (4:19) 141 | 142 | Start todoList.changeTodo (2:09) 143 | 144 | Start todoList.deleteTodo (2:04) 145 | 146 | Start todoList.toggleCompleted (2:25) 147 | 148 | Start todoList.toggleAll (4:37) 149 | 150 | Start Use the debugger all the time (1:54) 151 | Version 8 - Getting data from inputs 152 | 153 | Start Our first refactoring (9:45) 154 | 155 | Start More on refactoring (3:29) 156 | 157 | Start Requirements (1:15) 158 | 159 | Start There should be a button for adding todos (7:51) 160 | 161 | Start There should be a button for changing todos (6:17) 162 | 163 | Start There should be a button for deleting todos (3:47) 164 | 165 | Start There should be a button for toggling a todo (4:04) 166 | 167 | Start Review (2:20) 168 | Version 9 - Escape from the console 169 | 170 | Start Requirements (2:29) 171 | 172 | Start Inserting li elements into the DOM (7:34) 173 | 174 | Start There should be an li element for every todo (8:48) 175 | 176 | Start Each li element should contain .todoText (2:49) 177 | 178 | Start Each li element should show .completed (7:38) 179 | 180 | Start Escaping the console (5:57) 181 | 182 | Start Review (3:49) 183 | Interlude - Functions inside of functions 184 | 185 | Start runWithDebugger (6:04) 186 | 187 | Start setTimeout (2:03) 188 | 189 | Start forEach (7:10) 190 | 191 | Start addEventListener (4:50) 192 | 193 | Start Buzzwords: Higher order functions and callback functions (4:17) 194 | Version 10 - Click to delete 195 | 196 | Start Introducing HyperDev (3:34) 197 | 198 | Start The 'return' statement (3:10) 199 | 200 | Start Requirements (1:48) 201 | 202 | Start There should be a way to create delete buttons (3:56) 203 | 204 | Start There should be a delete button for each todo (2:05) 205 | 206 | Start Each li should have an id that has the todo position (2:58) 207 | 208 | Start Delete buttons should have access to the todo id (6:14) 209 | 210 | Start Clicking delete should update todoList.todos and the DOM (8:39) 211 | 212 | Start Cleanup and Review (3:48) 213 | Version 11 - Destroy all for loops 214 | 215 | Start Requirements (0:37) 216 | 217 | Start todoList.toggleAll should use forEach (10:30) 218 | 219 | Start view.displayTodos should use forEach (7:20) 220 | 221 | Start Review (2:51) 222 | Next steps 223 | 224 | Start Congratulations! 225 | -------------------------------------------------------------------------------- /katas/course_time/js_course_time_scraper.py: -------------------------------------------------------------------------------- 1 | #!python3 2 | # js_course_time_scraper.py is a tool to scrape the html of the Watch and Code 3 | # JS course to see how long the actual course is in total. It's not listed 4 | # on the course page/site anywhere thus the necessity of this tool. 5 | # 6 | # update 4th of Feb 2018: solved bug and added more elegant way = datetime 7 | 8 | from datetime import datetime, timedelta 9 | import re 10 | 11 | HTML_FILE = "content.html" 12 | 13 | 14 | def get_all_timestamps(): 15 | with open(HTML_FILE) as f: 16 | content = f.read() 17 | return re.findall(r'\d+:\d+', content) 18 | 19 | 20 | def calc_duration(timings): 21 | total_seconds = 0 22 | for mm_ss in timings: 23 | minutes, seconds = mm_ss.split(':') 24 | total_seconds += int(minutes) * 60 + int(seconds) 25 | 26 | minutes, seconds = divmod(total_seconds, 60) 27 | hours, minutes = divmod(minutes, 60) 28 | 29 | return f'{hours}:{minutes}:{seconds}' 30 | 31 | 32 | def calc_duration_improved(timings): 33 | start = datetime.now() 34 | end = datetime.now() 35 | 36 | for mm_ss in timings: 37 | minutes, seconds = mm_ss.split(':') 38 | end += timedelta(minutes=int(minutes), seconds=int(seconds)) 39 | 40 | return str(end - start) 41 | 42 | 43 | if __name__ == "__main__": 44 | timings = get_all_timestamps() 45 | 46 | # 1. using counting + divmod 47 | course_total = calc_duration(timings) 48 | assert str(course_total) == '6:50:31' 49 | 50 | # 2. using datetime (nicer) 51 | course_total = calc_duration_improved(timings) 52 | assert '6:50:31' in course_total 53 | 54 | print(f'The course takes {course_total} to complete') 55 | -------------------------------------------------------------------------------- /katas/pypi/.gitignore: -------------------------------------------------------------------------------- 1 | feed.rss 2 | pypi.html 3 | *.log 4 | -------------------------------------------------------------------------------- /katas/pypi/README.md: -------------------------------------------------------------------------------- 1 | ## PyPI reaching 100K packages 2 | 3 | Attempt to solve [Raymond Hettinger's data extrapolation contest](https://twitter.com/raymondh/status/829474817082433536) 4 | -------------------------------------------------------------------------------- /katas/pypi/download_feed.py: -------------------------------------------------------------------------------- 1 | import urllib2 2 | import time 3 | 4 | FEED = 'https://pypi.python.org/pypi?%3Aaction=packages_rss' 5 | NOW = str(int(time.time())) 6 | FILE_NAME = 'feed_{}.rss'.format(NOW) 7 | 8 | response = urllib2.urlopen(FEED) 9 | html = response.read() 10 | with open(FILE_NAME, 'w') as f: 11 | f.write(html) 12 | -------------------------------------------------------------------------------- /katas/pypi/pypi100k.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import logging 3 | import os 4 | from time import mktime 5 | import sys 6 | 7 | import bs4 8 | import feedparser 9 | import requests 10 | 11 | NUM_PACKS_TO_REACH = 100000 12 | NOW = datetime.utcnow() 13 | PYPI = 'https://pypi.python.org/pypi' 14 | PYPI_OUT = 'pypi.html' 15 | RSS = 'https://pypi.python.org/pypi?%3Aaction=packages_rss' 16 | RSS_OUT = 'feed.rss' 17 | LOGFILE = 'pypi.log' 18 | REFRESH = True 19 | 20 | 21 | logFormatter = logging.Formatter("%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s") 22 | rootLogger = logging.getLogger() 23 | rootLogger.setLevel(logging.INFO) # ignore imported module's debug messages 24 | 25 | fileHandler = logging.FileHandler(LOGFILE) 26 | fileHandler.setFormatter(logFormatter) 27 | rootLogger.addHandler(fileHandler) 28 | 29 | consoleHandler = logging.StreamHandler() 30 | consoleHandler.setFormatter(logFormatter) 31 | rootLogger.addHandler(consoleHandler) 32 | 33 | 34 | def get_feed(rss, fname): 35 | r = requests.get(rss) 36 | if r.status_code != 200: 37 | sys.exit('cannot get feed') 38 | with open(fname, 'w') as f: 39 | f.write(r.text) 40 | 41 | 42 | def get_current_num_packages(): 43 | if not os.path.isfile(PYPI_OUT) or REFRESH: 44 | get_feed(PYPI, PYPI_OUT) 45 | with open(PYPI_OUT) as f: 46 | soup = bs4.BeautifulSoup(f.read(), "lxml") 47 | div = soup.find('div', {"class":"section"}) 48 | try: 49 | return int(div.find('strong').text) 50 | except: 51 | sys.exit('Cannot scrape number of package') 52 | 53 | 54 | def main(): 55 | if not os.path.isfile(RSS_OUT) or REFRESH: 56 | get_feed(RSS, RSS_OUT) 57 | 58 | num_packages = get_current_num_packages() 59 | logging.info('Now there are {} packages'.format(num_packages)) 60 | 61 | with open(RSS_OUT) as f: 62 | html = f.read() 63 | items = feedparser.parse(html) 64 | dates = [datetime.fromtimestamp(mktime(item['published_parsed'])) for item in items['entries']] 65 | maxdate = max(dates) 66 | mindate = min(dates) 67 | logging.info('RSS new packages: min date = {} / max date = {}'.format(mindate, maxdate)) 68 | avg_addtime = (maxdate - mindate) / len(dates) 69 | logging.info('Avg time between additions: {}'.format(avg_addtime)) 70 | packages_to_be_added = NUM_PACKS_TO_REACH - num_packages 71 | logging.info('Packages to be added: {}'.format(packages_to_be_added)) 72 | time_till_reach = avg_addtime * packages_to_be_added 73 | logging.info('Time till reach = {}'.format(time_till_reach)) 74 | endresult = NOW + time_till_reach 75 | logging.info('Result (NOW + time till reach): {}'.format(endresult)) 76 | 77 | if __name__ == "__main__": 78 | main() 79 | -------------------------------------------------------------------------------- /katas/pypi/requirements.txt: -------------------------------------------------------------------------------- 1 | bs4 2 | feedparser 3 | lxml 4 | requests 5 | -------------------------------------------------------------------------------- /katas/pypi2/pypirolls.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import ssl 3 | import time 4 | try: 5 | import xmlrpclib 6 | except ImportError: 7 | import xmlrpc.client as xmlrpclib 8 | 9 | GOAL = 100000 10 | TIMEOUT = 5 * 60 * 60 11 | 12 | client = xmlrpclib.ServerProxy('https://pypi.python.org/pypi', 13 | context=ssl._create_unverified_context()) 14 | 15 | while True: 16 | now = datetime.datetime.now() 17 | packages = client.list_packages() 18 | num_packages = len(packages) 19 | print('It is {} and PyPI has {} packages'.format(now, num_packages)) 20 | if num_packages >= GOAL: 21 | print('Reached goal!') 22 | break 23 | time.sleep(TIMEOUT) 24 | -------------------------------------------------------------------------------- /katas/vowel_count/vowels.py: -------------------------------------------------------------------------------- 1 | VOWELS = list('aeiou') 2 | 3 | 4 | def get_word(): 5 | return input('What is our word? ') 6 | 7 | 8 | def count_vowels(string): 9 | count = 0 10 | for char in string: 11 | if char.lower() in VOWELS: 12 | count += 1 13 | return count 14 | 15 | 16 | def count_vowels_oneline(string): 17 | return sum(1 for char in string if char.lower() in VOWELS) 18 | 19 | 20 | def test(): 21 | tests = dict(( 22 | ('bob', 1), ('vowel', 2), ('house', 3), 23 | ('HOUSe', 3), ('pybit.ES', 2), 24 | ('how is it going MATE?', 7), 25 | )) 26 | for s, result in tests.items(): 27 | assert count_vowels(s) == result 28 | assert count_vowels_oneline(s) == result 29 | 30 | 31 | if __name__ == "__main__": 32 | test() 33 | print('This program will count the number of vowels in that word. ') 34 | word = get_word() 35 | print(word) 36 | count = count_vowels(word) 37 | print('The number of vowels in {} is: {}'.format(word, count)) 38 | -------------------------------------------------------------------------------- /kindle_notes/.gitignore: -------------------------------------------------------------------------------- 1 | *.json 2 | *.html 3 | -------------------------------------------------------------------------------- /kindle_notes/kindle_json2html.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | 4 | from templates import PAGE, QUOTE 5 | 6 | JSON_EXT = ".json" 7 | HTML_EXT = ".html" 8 | 9 | def load_json(json_file): 10 | with open(json_file) as f: 11 | return json.loads(f.read()) 12 | 13 | def get_highlights(highlights): 14 | for hl in highlights: 15 | yield QUOTE.safe_substitute({ 16 | 'text' : hl['text'], 17 | 'note' : ' / note: ' + hl['note'] if hl['note'] else '', 18 | 'url' : hl['location']['url'], 19 | 'location': hl['location']['value'], 20 | }) 21 | 22 | 23 | if __name__ == "__main__": 24 | if len(sys.argv) < 2: 25 | sys.exit("Usage: {} ".format(sys.argv[0])) 26 | 27 | for json_file in sys.argv[1:]: 28 | if not json_file.endswith(JSON_EXT): 29 | print("{} is not a json file".format(json_file)) 30 | continue 31 | 32 | html_out = json_file.replace(JSON_EXT, HTML_EXT) 33 | 34 | content = load_json(json_file) 35 | highlights = get_highlights(content['highlights']) 36 | with open(html_out, 'w') as f: 37 | f.write(PAGE.safe_substitute({ 38 | 'asin': content['asin'], 39 | 'title': content['title'], 40 | 'author': content['authors'], 41 | 'content': '\n'.join(list(highlights)), 42 | })) 43 | print("{} created".format(html_out)) 44 | -------------------------------------------------------------------------------- /kindle_notes/templates.py: -------------------------------------------------------------------------------- 1 | from string import Template 2 | 3 | PAGE = Template(''' 4 | 5 | 6 | 7 | 8 | $title 9 | 10 | 11 | 41 | 42 | 43 | 44 |
45 |

$title

46 |

by $author | Amazon

47 | $title 48 |
49 |
50 | $content 51 |
52 | 53 | ''') 54 | 55 | QUOTE = Template(''' 56 |
  • 57 |
    $text (loc: $location$note)
    58 |
  • ''') 59 | -------------------------------------------------------------------------------- /notebooks/README.md: -------------------------------------------------------------------------------- 1 | ## Jupyter notebooks 2 | 3 | In this directory we store our notebooks. 4 | 5 | Some [appeared on the blog](https://github.com/pybites/pybites.github.io-src/tree/master/content), these are linked below: 6 | 7 | * [Everything is an Object, Python OOP primer](http://pybit.es/oop-primer.html) 8 | 9 | * [Python Data model](http://pybit.es/python-data-model.html) 10 | 11 | * [Visualizing website and social media metrics with matplotlib](http://pybit.es/matplotlib-starter.html) 12 | 13 | -------------------------------------------------------------------------------- /notebooks/itertools.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 91, 6 | "metadata": { 7 | "collapsed": true 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "import itertools\n", 12 | "import random\n", 13 | "from collections import Counter\n", 14 | "from operator import itemgetter" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "# 1. Use product to get all combinations between two iterators" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 92, 27 | "metadata": { 28 | "collapsed": true 29 | }, 30 | "outputs": [], 31 | "source": [ 32 | "ranks = [str(n) for n in range(2, 11)] + list('JQKA')" 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": 93, 38 | "metadata": { 39 | "collapsed": true 40 | }, 41 | "outputs": [], 42 | "source": [ 43 | "suits = 'S D C H'.split() # spades diamonds clubs hearts" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": 94, 49 | "metadata": { 50 | "collapsed": false 51 | }, 52 | "outputs": [ 53 | { 54 | "name": "stdout", 55 | "output_type": "stream", 56 | "text": [ 57 | "['S2', 'S3', 'S4']\n", 58 | "['HQ', 'HK', 'HA']\n" 59 | ] 60 | } 61 | ], 62 | "source": [ 63 | "# double list comprehension as in fluent Python book\n", 64 | "cards = ['{}{}'.format(suit, rank) for suit in suits for rank in ranks]\n", 65 | "print(cards[:3]); print(cards[-3:])" 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": 95, 71 | "metadata": { 72 | "collapsed": false 73 | }, 74 | "outputs": [ 75 | { 76 | "data": { 77 | "text/plain": [ 78 | "52" 79 | ] 80 | }, 81 | "execution_count": 95, 82 | "metadata": {}, 83 | "output_type": "execute_result" 84 | } 85 | ], 86 | "source": [ 87 | "len(cards)" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": 114, 93 | "metadata": { 94 | "collapsed": false 95 | }, 96 | "outputs": [], 97 | "source": [ 98 | "# alternative way = itertools.product\n", 99 | "cards2 = ['{}{}'.format(*p) for p in itertools.product(suits, ranks)]" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": 97, 105 | "metadata": { 106 | "collapsed": false 107 | }, 108 | "outputs": [ 109 | { 110 | "data": { 111 | "text/plain": [ 112 | "52" 113 | ] 114 | }, 115 | "execution_count": 97, 116 | "metadata": {}, 117 | "output_type": "execute_result" 118 | } 119 | ], 120 | "source": [ 121 | "len(cards2)" 122 | ] 123 | }, 124 | { 125 | "cell_type": "code", 126 | "execution_count": 98, 127 | "metadata": { 128 | "collapsed": false 129 | }, 130 | "outputs": [], 131 | "source": [ 132 | "assert cards2 == cards" 133 | ] 134 | }, 135 | { 136 | "cell_type": "code", 137 | "execution_count": 99, 138 | "metadata": { 139 | "collapsed": false 140 | }, 141 | "outputs": [ 142 | { 143 | "name": "stdout", 144 | "output_type": "stream", 145 | "text": [ 146 | "(5, 2)\n", 147 | "(2, 5)\n", 148 | "(2, 5)\n", 149 | "(6, 5)\n" 150 | ] 151 | } 152 | ], 153 | "source": [ 154 | "# or use itertools to roll two dices\n", 155 | "# http://stackoverflow.com/questions/3099987/generating-permutations-with-repetitions-in-python\n", 156 | "dice = range(1, 7)\n", 157 | "for _ in range(4):\n", 158 | " roll = random.choice([p for p in itertools.product(dice, repeat=2)])\n", 159 | " print(roll)" 160 | ] 161 | }, 162 | { 163 | "cell_type": "markdown", 164 | "metadata": {}, 165 | "source": [ 166 | "## 2. Show a progress spinner for a console app" 167 | ] 168 | }, 169 | { 170 | "cell_type": "markdown", 171 | "metadata": {}, 172 | "source": [ 173 | "From [this awesome itertools preso](https://github.com/vterron/EuroPython-2016/blob/master/kung-fu-itertools.ipynb)" 174 | ] 175 | }, 176 | { 177 | "cell_type": "code", 178 | "execution_count": 100, 179 | "metadata": { 180 | "collapsed": false 181 | }, 182 | "outputs": [ 183 | { 184 | "name": "stdout", 185 | "output_type": "stream", 186 | "text": [ 187 | "Please wait... \\\n" 188 | ] 189 | } 190 | ], 191 | "source": [ 192 | "import itertools\n", 193 | "import sys\n", 194 | "import time\n", 195 | "\n", 196 | "def spinner(seconds):\n", 197 | " \"\"\"Show an animated spinner while we sleep.\"\"\"\n", 198 | " symbols = itertools.cycle('-\\|/')\n", 199 | " tend = time.time() + seconds\n", 200 | " while time.time() < tend:\n", 201 | " # '\\r' is carriage return: return cursor to the start of the line.\n", 202 | " sys.stdout.write('\\rPlease wait... ' + next(symbols)) # no newline\n", 203 | " sys.stdout.flush()\n", 204 | " time.sleep(0.1)\n", 205 | " print() # newline\n", 206 | "\n", 207 | "if __name__ == \"__main__\":\n", 208 | " spinner(3)" 209 | ] 210 | }, 211 | { 212 | "cell_type": "markdown", 213 | "metadata": {}, 214 | "source": [ 215 | "## 3. Use dropwhile to get counts of >= n in a Counter dict\n", 216 | "\n", 217 | "From a [code kata](http://bobbelderbos.com/2016/12/code-kata/) I did: get only books that occurred 2 or more times." 218 | ] 219 | }, 220 | { 221 | "cell_type": "code", 222 | "execution_count": 101, 223 | "metadata": { 224 | "collapsed": false 225 | }, 226 | "outputs": [], 227 | "source": [ 228 | "books = Counter({'atlas-shrugged-ayn-rand': 3,\n", 229 | " 'tao-te-ching-laozi': 3,\n", 230 | " 'influence-psychology-persuasion-robert-cialdini': 2,\n", 231 | " 'black-swan-improbable-robustness-fragility': 2,\n", 232 | " 'zero-one-notes-startups-future': 2,\n", 233 | " 'hard-thing-about-things-building': 2,\n", 234 | " 'unbearable-lightness-being-milan-kundera': 2,\n", 235 | " 'alchemist-paulo-coelho': 2,\n", 236 | " 'hour-body-uncommon-incredible-superhuman': 2,\n", 237 | " '4-hour-workweek-escape-live-anywhere': 2,\n", 238 | " 'drama-gifted-child-search-revised': 2,\n", 239 | " 'shogun-james-clavell': 2,\n", 240 | " 'checklist-manifesto-how-things-right': 2,\n", 241 | " 'sapiens-humankind-yuval-noah-harari': 2,\n", 242 | " 'war-art-through-creative-battles': 2,\n", 243 | " 'what-makes-sammy-budd-schulberg': 2,\n", 244 | " 'mindset-psychology-carol-s-dweck': 2,\n", 245 | " 'jesus-son-stories-denis-johnson': 1,\n", 246 | " 'kite-runner-khaled-hosseini': 1,\n", 247 | " 'thousand-splendid-suns-khaled-hosseini': 1,\n", 248 | " 'antifragile-things-that-disorder-incerto': 1,\n", 249 | " 'fooled-randomness-hidden-markets-incerto': 1,\n", 250 | " 'brain-rules-principles-surviving-thriving': 1,\n", 251 | " 'outliers-story-success-malcolm-gladwell': 1,\n", 252 | " 'freakonomics-economist-explores-hidden-everything': 1,\n", 253 | " 'high-output-management-andrew-grove': 1,\n", 254 | " 'only-paranoid-survive-exploit-challenge': 1,\n", 255 | " 'walt-disney-triumph-american-imagination': 1,\n", 256 | " 'schulz-peanuts-biography-david-michaelis': 1,\n", 257 | " 'wizard-menlo-park-thomas-invented': 1,\n", 258 | " 'born-standing-up-comics-life': 1,\n", 259 | " 'mistakes-were-made-but-not': 1,\n", 260 | " 'surely-feynman-adventures-curious-character': 1,\n", 261 | " '10-happier-self-help-actually-works': 1,\n", 262 | " 'book-virtues-william-j-bennett': 1,\n", 263 | " 'winners-never-cheat-difficult-expanded': 1,\n", 264 | " 'coan-man-myth-method-powerlifter-ebook': 1,\n", 265 | " 'lifes-little-instruction-book-observations': 1,\n", 266 | " 'fans-notes-frederick-exley': 1,\n", 267 | " 'crossroads-should-must-follow-passion': 1,\n", 268 | " 'promise-sleep-medicine-connection-happiness': 1,\n", 269 | " 'house-leaves-mark-z-danielewski': 1,\n", 270 | " 'musicophilia-tales-music-revised-expanded': 1,\n", 271 | " 'waking-up-spirituality-without-religion': 1,\n", 272 | " 'this-your-brain-music-obsession': 1,\n", 273 | " 'excellent-sheep-miseducation-american-meaningful': 1,\n", 274 | " 'fountainhead-ayn-rand': 1,\n", 275 | " 'thousand-faces-collected-joseph-campbell': 1,\n", 276 | " 'genealogy-morals-oxford-worlds-classics': 1,\n", 277 | " 'art-learning-journey-optimal-performance': 1,\n", 278 | " 'bad-science-quacks-pharma-flacks': 1,\n", 279 | " 'bad-pharma-companies-mislead-patients': 1,\n", 280 | " 'fiasco-american-military-adventure-iraq': 1,\n", 281 | " 'looming-tower-al-qaeda-road-11': 1,\n", 282 | " 'going-clear-scientology-hollywood-prison': 1,\n", 283 | " 'plato-symposium-benjamin-jowett': 1,\n", 284 | " 'musashi-epic-novel-samurai-era': 1,\n", 285 | " 'guide-ching-carol-k-anthony': 1,\n", 286 | " 'missoula-rape-justice-system-college': 1,\n", 287 | " 'how-movie-star-elizabeth-hollywood': 1,\n", 288 | " 'super-sad-true-love-story': 1,\n", 289 | " 'fantasy-bond-structure-psychological-defenses': 1,\n", 290 | " 'continuum-concept-happiness-classics-development': 1,\n", 291 | " 'personal-power-classic-anthony-robbins': 1,\n", 292 | " 'tripping-over-truth-metabolic-illuminates': 1,\n", 293 | " 'language-god-scientist-presents-evidence': 1,\n", 294 | " 'screwtape-letters-c-s-lewis': 1,\n", 295 | " 'cancer-metabolic-disease-management-prevention': 1,\n", 296 | " 'complete-essays-montaigne-illustrated-ebook': 1,\n", 297 | " 'search-lost-time-proust-complete': 1,\n", 298 | " 'minute-manager-kenneth-blanchard-ph-d': 1,\n", 299 | " 'levels-game-john-mcphee': 1,\n", 300 | " 'empty-pot-owlet-book': 1,\n", 301 | " 'national-geographic-field-guide-america': 1,\n", 302 | " 'pihkal-chemical-story-alexander-shulgin': 1,\n", 303 | " 'tihkal-continuation-alexander-shulgin': 1,\n", 304 | " 'writers-journey-mythic-structure-3rd': 1,\n", 305 | " 'would-nice-you-werent-here': 1,\n", 306 | " 'hobbit-j-r-tolkien': 1,\n", 307 | " 'kitchen-confidential-updated-adventures-underbelly': 1,\n", 308 | " 'without-sanctuary-lynching-photography-america': 1,\n", 309 | " 'hundred-solitude-harper-perennial-classics': 1,\n", 310 | " 'between-world-me-ta-nehisi-coates': 1,\n", 311 | " 'speak-like-churchill-stand-lincoln': 1,\n", 312 | " 'feast-snakes-novel-harry-crews': 1,\n", 313 | " 'car-novel-harry-crews': 1,\n", 314 | " 'dont-make-me-think-usability': 1,\n", 315 | " 'how-measure-anything-intangibles-business': 1,\n", 316 | " 'how-not-be-wrong-mathematical': 1,\n", 317 | " 'getting-yes-negotiating-agreement-without': 1,\n", 318 | " 'foundation-isaac-asimov': 1,\n", 319 | " 'reality-dysfunction-nights-dawn': 1,\n", 320 | " 'mountain-light-search-dynamic-landscape': 1,\n", 321 | " 'strangers-ourselves-discovering-adaptive-unconscious': 1,\n", 322 | " 'merchant-princes-intimate-families-department': 1,\n", 323 | " 'tinker-tailor-soldier-spy-george': 1,\n", 324 | " 'little-drummer-girl-novel': 1,\n", 325 | " 'russia-house-novel-john-carre': 1,\n", 326 | " 'spy-who-came-cold-george': 1,\n", 327 | " 'big-short-inside-doomsday-machine': 1,\n", 328 | " 'lee-child': 1,\n", 329 | " 'natural-born-heroes-mastering-endurance': 1,\n", 330 | " 'hobbit-lord-rings-fellowship-towers': 1,\n", 331 | " 'deep-survival-who-lives-dies': 1,\n", 332 | " 'jonathan-livingston-seagull-richard-bach': 1,\n", 333 | " 'dune-frank-herbert': 1,\n", 334 | " 'conscious-business-build-through-values': 1,\n", 335 | " 'meditations-modern-library-classics-aurelius': 1,\n", 336 | " 'titan-life-john-rockefeller-sr': 1,\n", 337 | " 'how-live-montaigne-question-attempts': 1,\n", 338 | " 'fish-that-ate-whale-americas': 1,\n", 339 | " 'tough-jews-fathers-gangster-dreams': 1,\n", 340 | " 'edison-biography-matthew-josephson': 1,\n", 341 | " 'ulysses-s-grant-adversity-1822-1865': 1,\n", 342 | " 'fahrenheit-451-ray-bradbury': 1,\n", 343 | " 'play-fields-lord-peter-matthiessen': 1,\n", 344 | " 'lights-out-cyberattack-unprepared-surviving': 1,\n", 345 | " 'artists-way-morning-pages-journal': 1,\n", 346 | " 'once-eagle-anton-myrer': 1,\n", 347 | " 'road-character-david-brooks': 1,\n", 348 | " 'its-not-how-good-want': 1,\n", 349 | " 'second-world-war-john-keegan': 1,\n", 350 | " 'autobiography-malcolm-told-alex-haley': 1,\n", 351 | " 'prophet-borzoi-book-kahlil-gibran': 1,\n", 352 | " 'wind-sand-stars-harvest-book': 1,\n", 353 | " 'buddhism-without-beliefs-contemporary-awakening': 1,\n", 354 | " 'search-modern-china-jonathan-spence': 1,\n", 355 | " 'death-woman-wang-jonathan-spence': 1,\n", 356 | " 'founders-work-stories-startups-early': 1,\n", 357 | " 'masters-doom-created-transformed-culture': 1,\n", 358 | " 'still-writing-perils-pleasures-creative': 1,\n", 359 | " 'shortness-life-penguin-great-ideas': 1,\n", 360 | " 'republic-plato': 1,\n", 361 | " 'move-life-oliver-sacks': 1,\n", 362 | " 'journal-thoreau-1837-1861-review-classics': 1,\n", 363 | " 'rap-race-james-baldwin': 1,\n", 364 | " 'science-necessity-love-god-essays': 1,\n", 365 | " 'stumbling-happiness-daniel-gilbert': 1,\n", 366 | " 'desert-solitaire-wilderness-edward-abbey': 1,\n", 367 | " 'gathering-moss-natural-cultural-history': 1,\n", 368 | " 'i-capture-castle-dodie-smith': 1,\n", 369 | " 'complete-short-stories-ernest-hemingway': 1,\n", 370 | " 'man-thinketh-life-changing-classics-pamphlet': 1,\n", 371 | " 'mans-search-meaning-viktor-frankl': 1,\n", 372 | " 'fourth-turning-american-prophecy-rendezvous': 1,\n", 373 | " 'generations-history-americas-future-1584': 1,\n", 374 | " 'slow-sex-craft-female-orgasm': 1,\n", 375 | " 'start-why-leaders-inspire-everyone': 1,\n", 376 | " 'miracle-mindfulness-introduction-practice-meditation': 1,\n", 377 | " 'wisdom-crowds-james-surowiecki': 1,\n", 378 | " 'wherever-you-go-there-are': 1,\n", 379 | " 'churchill-factor-how-made-history': 1,\n", 380 | " 'free-choose-statement-milton-friedman': 1,\n", 381 | " 'california-history-modern-library-chronicles': 1,\n", 382 | " 'age-propaganda-everyday-abuse-persuasion': 1,\n", 383 | " 'social-animal-sources-character-achievement': 1,\n", 384 | " 'getting-everything-you-can-youve': 1,\n", 385 | " 'mindless-eating-more-than-think': 1,\n", 386 | " 'robert-collier-letter-book': 1,\n", 387 | " 'never-eat-alone-expanded-updated': 1,\n", 388 | " 'what-teach-harvard-business-school': 1,\n", 389 | " 'iacocca-autobiography-lee': 1,\n", 390 | " 'techgnosis-myth-magic-mysticism-information': 1,\n", 391 | " 'rise-superman-decoding-ultimate-performance': 1,\n", 392 | " 'cocktail-techniques-kazuo-uyeda': 1,\n", 393 | " 'obstacle-way-timeless-turning-triumph': 1,\n", 394 | " 'robert-heinlein': 1,\n", 395 | " 'what-buddha-taught-expanded-dhammapada': 1,\n", 396 | " 'buddhas-words-anthology-discourses-teachings': 1,\n", 397 | " 'things-hidden-since-foundation-world': 1,\n", 398 | " 'road-jack-kerouac': 1,\n", 399 | " 'dharma-bums-jack-kerouac': 1,\n", 400 | " 'zen-art-motorcycle-maintenance-inquiry': 1,\n", 401 | " 'shantaram-novel-gregory-david-roberts': 1,\n", 402 | " 'whom-bell-tolls-ernest-hemingway': 1,\n", 403 | " 'old-man-sea-ernest-hemingway': 1,\n", 404 | " 'green-hills-africa-hemingway-library': 1,\n", 405 | " 'ernest-hemingway-writing-larry-phillips': 1,\n", 406 | " 'dreaming-yourself-awake-tibetan-transformation': 1,\n", 407 | " 'tribe-homecoming-belonging-sebastian-junger': 1,\n", 408 | " 'grit-passion-perseverance-angela-duckworth': 1,\n", 409 | " 'peak-secrets-new-science-expertise-ebook': 1,\n", 410 | " 'about-face-odyssey-american-warrior': 1,\n", 411 | " 'blood-meridian-evening-redness-west': 1,\n", 412 | " 'tools-titans-billionaires-world-class-performers': 1})" 413 | ] 414 | }, 415 | { 416 | "cell_type": "code", 417 | "execution_count": 104, 418 | "metadata": { 419 | "collapsed": false 420 | }, 421 | "outputs": [ 422 | { 423 | "data": { 424 | "text/plain": [ 425 | "Counter({1: 168, 2: 15, 3: 2})" 426 | ] 427 | }, 428 | "execution_count": 104, 429 | "metadata": {}, 430 | "output_type": "execute_result" 431 | } 432 | ], 433 | "source": [ 434 | "Counter(books.values())" 435 | ] 436 | }, 437 | { 438 | "cell_type": "code", 439 | "execution_count": 105, 440 | "metadata": { 441 | "collapsed": true 442 | }, 443 | "outputs": [], 444 | "source": [ 445 | "# I want to discard books that appeared 1 time (168x), so only take >= 2 book counts\n", 446 | "def get_multiple_mentions(books, keep=2):\n", 447 | " for key, count in itertools.dropwhile(lambda key_count: key_count[1] >= keep, books.most_common()):\n", 448 | " del books[key]\n", 449 | " return books" 450 | ] 451 | }, 452 | { 453 | "cell_type": "code", 454 | "execution_count": 106, 455 | "metadata": { 456 | "collapsed": false 457 | }, 458 | "outputs": [ 459 | { 460 | "data": { 461 | "text/plain": [ 462 | "Counter({'4-hour-workweek-escape-live-anywhere': 2,\n", 463 | " 'alchemist-paulo-coelho': 2,\n", 464 | " 'atlas-shrugged-ayn-rand': 3,\n", 465 | " 'black-swan-improbable-robustness-fragility': 2,\n", 466 | " 'checklist-manifesto-how-things-right': 2,\n", 467 | " 'drama-gifted-child-search-revised': 2,\n", 468 | " 'hard-thing-about-things-building': 2,\n", 469 | " 'hour-body-uncommon-incredible-superhuman': 2,\n", 470 | " 'influence-psychology-persuasion-robert-cialdini': 2,\n", 471 | " 'mindset-psychology-carol-s-dweck': 2,\n", 472 | " 'sapiens-humankind-yuval-noah-harari': 2,\n", 473 | " 'shogun-james-clavell': 2,\n", 474 | " 'tao-te-ching-laozi': 3,\n", 475 | " 'unbearable-lightness-being-milan-kundera': 2,\n", 476 | " 'war-art-through-creative-battles': 2,\n", 477 | " 'what-makes-sammy-budd-schulberg': 2,\n", 478 | " 'zero-one-notes-startups-future': 2})" 479 | ] 480 | }, 481 | "execution_count": 106, 482 | "metadata": {}, 483 | "output_type": "execute_result" 484 | } 485 | ], 486 | "source": [ 487 | "books_filtered = get_multiple_mentions(books)\n", 488 | "assert sum(books_filtered.values()) == 36\n", 489 | "books_filtered" 490 | ] 491 | }, 492 | { 493 | "cell_type": "markdown", 494 | "metadata": {}, 495 | "source": [ 496 | "## 4. Combinations and permutations\n", 497 | "\n", 498 | "The difference is well explained in this article: [Easy Permutations and Combinations](https://betterexplained.com/articles/easy-permutations-and-combinations/)" 499 | ] 500 | }, 501 | { 502 | "cell_type": "code", 503 | "execution_count": 107, 504 | "metadata": { 505 | "collapsed": false 506 | }, 507 | "outputs": [ 508 | { 509 | "data": { 510 | "text/plain": [ 511 | "['bob', 'tim', 'julian', 'fred']" 512 | ] 513 | }, 514 | "execution_count": 107, 515 | "metadata": {}, 516 | "output_type": "execute_result" 517 | } 518 | ], 519 | "source": [ 520 | "friends = 'bob tim julian fred'.split()\n", 521 | "friends" 522 | ] 523 | }, 524 | { 525 | "cell_type": "code", 526 | "execution_count": 108, 527 | "metadata": { 528 | "collapsed": false 529 | }, 530 | "outputs": [ 531 | { 532 | "data": { 533 | "text/plain": [ 534 | "[('bob', 'tim'),\n", 535 | " ('bob', 'julian'),\n", 536 | " ('bob', 'fred'),\n", 537 | " ('tim', 'julian'),\n", 538 | " ('tim', 'fred'),\n", 539 | " ('julian', 'fred')]" 540 | ] 541 | }, 542 | "execution_count": 108, 543 | "metadata": {}, 544 | "output_type": "execute_result" 545 | } 546 | ], 547 | "source": [ 548 | "# how many pairs can you form among 4 friends? \n", 549 | "list(itertools.combinations(friends, 2))" 550 | ] 551 | }, 552 | { 553 | "cell_type": "code", 554 | "execution_count": 109, 555 | "metadata": { 556 | "collapsed": false 557 | }, 558 | "outputs": [], 559 | "source": [ 560 | "# if order would matter, you would get double results with permutations\n", 561 | "assert len(list(itertools.permutations(friends, 2))) == 12" 562 | ] 563 | }, 564 | { 565 | "cell_type": "code", 566 | "execution_count": 117, 567 | "metadata": { 568 | "collapsed": false 569 | }, 570 | "outputs": [ 571 | { 572 | "data": { 573 | "text/plain": [ 574 | "840" 575 | ] 576 | }, 577 | "execution_count": 117, 578 | "metadata": {}, 579 | "output_type": "execute_result" 580 | } 581 | ], 582 | "source": [ 583 | "# how many 4 letter strings can you from 7 letters?\n", 584 | "import string\n", 585 | "letters = random.sample(string.ascii_uppercase, 7)\n", 586 | "len(list(itertools.permutations(letters, 4)))" 587 | ] 588 | }, 589 | { 590 | "cell_type": "markdown", 591 | "metadata": { 592 | "collapsed": true 593 | }, 594 | "source": [ 595 | "## 5. Groupby to count amount of keys for specific value in dict\n", 596 | "\n", 597 | "Found this at [pymotw](https://pymotw.com/2/itertools/)\n", 598 | "Say you have a list of users with their preferred contact method, how do you get all email prefs easily?" 599 | ] 600 | }, 601 | { 602 | "cell_type": "code", 603 | "execution_count": 111, 604 | "metadata": { 605 | "collapsed": false 606 | }, 607 | "outputs": [ 608 | { 609 | "data": { 610 | "text/plain": [ 611 | "{'bob': 'phone',\n", 612 | " 'frank': 'email',\n", 613 | " 'fred': 'F2F',\n", 614 | " 'julian': 'IM',\n", 615 | " 'maria': 'phone',\n", 616 | " 'sue': 'email',\n", 617 | " 'tim': 'email'}" 618 | ] 619 | }, 620 | "execution_count": 111, 621 | "metadata": {}, 622 | "output_type": "execute_result" 623 | } 624 | ], 625 | "source": [ 626 | "users = 'tim bob julian sue fred frank maria'.split()\n", 627 | "prefs = 'email phone IM email F2F email phone'.split()\n", 628 | "user_prefs = dict(zip(users, prefs))\n", 629 | "user_prefs" 630 | ] 631 | }, 632 | { 633 | "cell_type": "code", 634 | "execution_count": 112, 635 | "metadata": { 636 | "collapsed": false 637 | }, 638 | "outputs": [ 639 | { 640 | "name": "stdout", 641 | "output_type": "stream", 642 | "text": [ 643 | "F2F ['fred']\n", 644 | "IM ['julian']\n", 645 | "email ['frank', 'tim', 'sue']\n", 646 | "phone ['bob', 'maria']\n" 647 | ] 648 | } 649 | ], 650 | "source": [ 651 | "user_prefs_sorted = sorted(user_prefs.items(), key=itemgetter(1))\n", 652 | "for pref, users in itertools.groupby(user_prefs_sorted, key=itemgetter(1)):\n", 653 | " print(pref, list(map(itemgetter(0), users)))" 654 | ] 655 | } 656 | ], 657 | "metadata": { 658 | "kernelspec": { 659 | "display_name": "Python 3", 660 | "language": "python", 661 | "name": "python3" 662 | }, 663 | "language_info": { 664 | "codemirror_mode": { 665 | "name": "ipython", 666 | "version": 3 667 | }, 668 | "file_extension": ".py", 669 | "mimetype": "text/x-python", 670 | "name": "python", 671 | "nbconvert_exporter": "python", 672 | "pygments_lexer": "ipython3", 673 | "version": "3.5.2" 674 | } 675 | }, 676 | "nbformat": 4, 677 | "nbformat_minor": 0 678 | } 679 | -------------------------------------------------------------------------------- /notebooks/pythonic-idioms.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "deletable": true, 7 | "editable": true 8 | }, 9 | "source": [ 10 | "
    \n", 11 | "

    PyBites Cheat Sheet

    \n", 12 | " 10 Pythonic Tips

    \n", 13 | "

    \n", 14 | " for i, tip in enumerate(pybites, 1): ... (= tip #0)\n", 15 | "
    \n", 16 | "\"PyBites\n" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 1, 22 | "metadata": { 23 | "collapsed": false, 24 | "deletable": true, 25 | "editable": true 26 | }, 27 | "outputs": [ 28 | { 29 | "data": { 30 | "text/plain": [ 31 | "'3.6.0 (v3.6.0:41df79263a11, Dec 22 2016, 17:23:13) \\n[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)]'" 32 | ] 33 | }, 34 | "execution_count": 1, 35 | "metadata": {}, 36 | "output_type": "execute_result" 37 | } 38 | ], 39 | "source": [ 40 | "from collections import Counter, defaultdict, namedtuple\n", 41 | "import itertools\n", 42 | "from pprint import pprint as pp\n", 43 | "from string import ascii_lowercase\n", 44 | "import sys\n", 45 | "import time\n", 46 | "\n", 47 | "sys.version" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": { 53 | "deletable": true, 54 | "editable": true 55 | }, 56 | "source": [ 57 | "### 1. Manage resources using the with statement" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 2, 63 | "metadata": { 64 | "collapsed": false, 65 | "deletable": true, 66 | "editable": true 67 | }, 68 | "outputs": [ 69 | { 70 | "data": { 71 | "text/plain": [ 72 | "['python', 'tips', 'tricks', 'resources', 'flask']" 73 | ] 74 | }, 75 | "execution_count": 2, 76 | "metadata": {}, 77 | "output_type": "execute_result" 78 | } 79 | ], 80 | "source": [ 81 | "# also using it first to get some data for later use\n", 82 | "with open('tags.txt') as f:\n", 83 | " tags = f.read().split()\n", 84 | "tags[:5]" 85 | ] 86 | }, 87 | { 88 | "cell_type": "markdown", 89 | "metadata": { 90 | "deletable": true, 91 | "editable": true 92 | }, 93 | "source": [ 94 | "References: [PCC09](http://pybit.es/codechallenge09_review.html) (PCC = PyBites Code Challenge)" 95 | ] 96 | }, 97 | { 98 | "cell_type": "markdown", 99 | "metadata": { 100 | "deletable": true, 101 | "editable": true 102 | }, 103 | "source": [ 104 | "### 2. Order a dict by value " 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": 3, 110 | "metadata": { 111 | "collapsed": true, 112 | "deletable": true, 113 | "editable": true 114 | }, 115 | "outputs": [], 116 | "source": [ 117 | "ages = {'julian': 20, 'bob': 23, 'zack': 3, 'anthony': 95, 'daniel': 41}" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": 4, 123 | "metadata": { 124 | "collapsed": false, 125 | "deletable": true, 126 | "editable": true 127 | }, 128 | "outputs": [ 129 | { 130 | "data": { 131 | "text/plain": [ 132 | "[('anthony', 95), ('daniel', 41), ('bob', 23), ('julian', 20), ('zack', 3)]" 133 | ] 134 | }, 135 | "execution_count": 4, 136 | "metadata": {}, 137 | "output_type": "execute_result" 138 | } 139 | ], 140 | "source": [ 141 | "sorted(ages.items(), key=lambda x: x[1], reverse=True)" 142 | ] 143 | }, 144 | { 145 | "cell_type": "markdown", 146 | "metadata": { 147 | "deletable": true, 148 | "editable": true 149 | }, 150 | "source": [ 151 | "Max/min also have the optional key argument:" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": 5, 157 | "metadata": { 158 | "collapsed": false, 159 | "deletable": true, 160 | "editable": true 161 | }, 162 | "outputs": [ 163 | { 164 | "data": { 165 | "text/plain": [ 166 | "('contextmanagers', 'hn')" 167 | ] 168 | }, 169 | "execution_count": 5, 170 | "metadata": {}, 171 | "output_type": "execute_result" 172 | } 173 | ], 174 | "source": [ 175 | "# longest vs shortest tag\n", 176 | "max(tags, key=len), min(tags, key=len)" 177 | ] 178 | }, 179 | { 180 | "cell_type": "markdown", 181 | "metadata": { 182 | "deletable": true, 183 | "editable": true 184 | }, 185 | "source": [ 186 | "References: [How to Order Dict Output](http://pybit.es/dict-ordering.html)" 187 | ] 188 | }, 189 | { 190 | "cell_type": "markdown", 191 | "metadata": { 192 | "deletable": true, 193 | "editable": true 194 | }, 195 | "source": [ 196 | "### 3. Tuple unpacking niceness" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": 6, 202 | "metadata": { 203 | "collapsed": true, 204 | "deletable": true, 205 | "editable": true 206 | }, 207 | "outputs": [], 208 | "source": [ 209 | "bob, julian = 'bob julian'.split()" 210 | ] 211 | }, 212 | { 213 | "cell_type": "code", 214 | "execution_count": 7, 215 | "metadata": { 216 | "collapsed": false, 217 | "deletable": true, 218 | "editable": true 219 | }, 220 | "outputs": [ 221 | { 222 | "data": { 223 | "text/plain": [ 224 | "('bob', 'julian')" 225 | ] 226 | }, 227 | "execution_count": 7, 228 | "metadata": {}, 229 | "output_type": "execute_result" 230 | } 231 | ], 232 | "source": [ 233 | "bob, julian" 234 | ] 235 | }, 236 | { 237 | "cell_type": "code", 238 | "execution_count": 8, 239 | "metadata": { 240 | "collapsed": true, 241 | "deletable": true, 242 | "editable": true 243 | }, 244 | "outputs": [], 245 | "source": [ 246 | "a, *b, c = [1, 2, 3, 4, 5]" 247 | ] 248 | }, 249 | { 250 | "cell_type": "code", 251 | "execution_count": 9, 252 | "metadata": { 253 | "collapsed": false, 254 | "deletable": true, 255 | "editable": true 256 | }, 257 | "outputs": [ 258 | { 259 | "data": { 260 | "text/plain": [ 261 | "(1, [2, 3, 4], 5)" 262 | ] 263 | }, 264 | "execution_count": 9, 265 | "metadata": {}, 266 | "output_type": "execute_result" 267 | } 268 | ], 269 | "source": [ 270 | "a, b, c" 271 | ] 272 | }, 273 | { 274 | "cell_type": "code", 275 | "execution_count": 10, 276 | "metadata": { 277 | "collapsed": true, 278 | "deletable": true, 279 | "editable": true 280 | }, 281 | "outputs": [], 282 | "source": [ 283 | "a = 'hello'\n", 284 | "b = 'world'\n", 285 | "a, b = b, a" 286 | ] 287 | }, 288 | { 289 | "cell_type": "code", 290 | "execution_count": 11, 291 | "metadata": { 292 | "collapsed": false, 293 | "deletable": true, 294 | "editable": true 295 | }, 296 | "outputs": [ 297 | { 298 | "data": { 299 | "text/plain": [ 300 | "('world', 'hello')" 301 | ] 302 | }, 303 | "execution_count": 11, 304 | "metadata": {}, 305 | "output_type": "execute_result" 306 | } 307 | ], 308 | "source": [ 309 | "a, b" 310 | ] 311 | }, 312 | { 313 | "cell_type": "code", 314 | "execution_count": 12, 315 | "metadata": { 316 | "collapsed": false, 317 | "deletable": true, 318 | "editable": true 319 | }, 320 | "outputs": [ 321 | { 322 | "name": "stdout", 323 | "output_type": "stream", 324 | "text": [ 325 | "julian 20\n", 326 | "bob 23\n", 327 | "zack 3\n", 328 | "anthony 95\n", 329 | "daniel 41\n" 330 | ] 331 | } 332 | ], 333 | "source": [ 334 | "for name, age in ages.items():\n", 335 | " print(name, age)" 336 | ] 337 | }, 338 | { 339 | "cell_type": "markdown", 340 | "metadata": { 341 | "deletable": true, 342 | "editable": true 343 | }, 344 | "source": [ 345 | "References: [Python Iteration](http://pybit.es/python_iteration.html), [Beautiful Python](http://pybit.es/beautiful-python.html), [Daily Python Tip](https://twitter.com/python_tip/status/836803438784700416)" 346 | ] 347 | }, 348 | { 349 | "cell_type": "markdown", 350 | "metadata": { 351 | "deletable": true, 352 | "editable": true 353 | }, 354 | "source": [ 355 | "### 4. Combine collections with zip" 356 | ] 357 | }, 358 | { 359 | "cell_type": "code", 360 | "execution_count": 13, 361 | "metadata": { 362 | "collapsed": false, 363 | "deletable": true, 364 | "editable": true 365 | }, 366 | "outputs": [ 367 | { 368 | "name": "stdout", 369 | "output_type": "stream", 370 | "text": [ 371 | "julian 20\n", 372 | "bob 23\n", 373 | "zack 3\n", 374 | "anthony 95\n", 375 | "daniel 41\n" 376 | ] 377 | } 378 | ], 379 | "source": [ 380 | "names = ('julian', 'bob', 'zack', 'anthony', 'daniel')\n", 381 | "ages = (20, 23, 3, 95, 41)\n", 382 | "for name, age in zip(names, ages):\n", 383 | " print('{:<10}{}'.format(name, age))" 384 | ] 385 | }, 386 | { 387 | "cell_type": "markdown", 388 | "metadata": { 389 | "deletable": true, 390 | "editable": true 391 | }, 392 | "source": [ 393 | "References: [Beautiful Python](http://pybit.es/beautiful-python.html), used a couple of times in [Matplotlib primer](http://pybit.es/matplotlib-starter.html)." 394 | ] 395 | }, 396 | { 397 | "cell_type": "markdown", 398 | "metadata": { 399 | "deletable": true, 400 | "editable": true 401 | }, 402 | "source": [ 403 | "### 5. Collections.namedtuple" 404 | ] 405 | }, 406 | { 407 | "cell_type": "markdown", 408 | "metadata": { 409 | "deletable": true, 410 | "editable": true 411 | }, 412 | "source": [ 413 | "Named tuples: readable, convenient, like classes without behavior:" 414 | ] 415 | }, 416 | { 417 | "cell_type": "code", 418 | "execution_count": 14, 419 | "metadata": { 420 | "collapsed": false, 421 | "deletable": true, 422 | "editable": true 423 | }, 424 | "outputs": [], 425 | "source": [ 426 | "Tweet = namedtuple('Tweet', 'id_str created_at text')\n", 427 | "now = int(time.time())\n", 428 | "tweet = Tweet('123', now, 'Python is awesome')" 429 | ] 430 | }, 431 | { 432 | "cell_type": "code", 433 | "execution_count": 15, 434 | "metadata": { 435 | "collapsed": true, 436 | "deletable": true, 437 | "editable": true 438 | }, 439 | "outputs": [], 440 | "source": [ 441 | "Item = namedtuple('Item', 'name value')\n", 442 | "item = Item('tv', 600)" 443 | ] 444 | }, 445 | { 446 | "cell_type": "markdown", 447 | "metadata": { 448 | "deletable": true, 449 | "editable": true 450 | }, 451 | "source": [ 452 | "References: [PCC04](http://pybit.es/codechallenge04_review.html), [PCC08](https://github.com/pybites/challenges/blob/solutions/08/inventory_bob.py)" 453 | ] 454 | }, 455 | { 456 | "cell_type": "markdown", 457 | "metadata": { 458 | "deletable": true, 459 | "editable": true 460 | }, 461 | "source": [ 462 | "### 6. Collections.defaultdict and Counter" 463 | ] 464 | }, 465 | { 466 | "cell_type": "code", 467 | "execution_count": 16, 468 | "metadata": { 469 | "collapsed": false, 470 | "deletable": true, 471 | "editable": true 472 | }, 473 | "outputs": [ 474 | { 475 | "data": { 476 | "text/plain": [ 477 | "[('python', 10),\n", 478 | " ('code', 8),\n", 479 | " ('learning', 7),\n", 480 | " ('tips', 6),\n", 481 | " ('tricks', 5),\n", 482 | " ('challenges', 5),\n", 483 | " ('github', 5),\n", 484 | " ('data', 5),\n", 485 | " ('cleancode', 5),\n", 486 | " ('best', 5)]" 487 | ] 488 | }, 489 | "execution_count": 16, 490 | "metadata": {}, 491 | "output_type": "execute_result" 492 | } 493 | ], 494 | "source": [ 495 | "most_common_tags = Counter(tags).most_common(10)\n", 496 | "most_common_tags" 497 | ] 498 | }, 499 | { 500 | "cell_type": "code", 501 | "execution_count": 17, 502 | "metadata": { 503 | "collapsed": false, 504 | "deletable": true, 505 | "editable": true 506 | }, 507 | "outputs": [ 508 | { 509 | "name": "stdout", 510 | "output_type": "stream", 511 | "text": [ 512 | "defaultdict(,\n", 513 | " {'kitchen': [Item(name='table', value=300)],\n", 514 | " 'living': [Item(name='tv', value=600),\n", 515 | " Item(name='sofa', value=500)],\n", 516 | " 'study': [Item(name='chair', value=200)]})\n" 517 | ] 518 | } 519 | ], 520 | "source": [ 521 | "# defaultdict, using previously defined Item namedtuple and zip idiom\n", 522 | "inventory = defaultdict(list)\n", 523 | "\n", 524 | "things = ('tv', 'sofa', 'table', 'chair')\n", 525 | "values = (600, 500, 300, 200)\n", 526 | "rooms = ('living', 'living', 'kitchen', 'study')\n", 527 | "\n", 528 | "for name, age, room in zip(things, values, rooms):\n", 529 | " item = Item(name, age)\n", 530 | " inventory[room].append(item)\n", 531 | "\n", 532 | "pp(inventory)" 533 | ] 534 | }, 535 | { 536 | "cell_type": "markdown", 537 | "metadata": { 538 | "deletable": true, 539 | "editable": true 540 | }, 541 | "source": [ 542 | "References: [PCC03](http://pybit.es/codechallenge03_review.html), [PCC07](http://pybit.es/codechallenge07_review.html), [PCC08](https://github.com/pybites/challenges/blob/solutions/08/inventory_bob.py), [bobcodes.it](http://bobbelderbos.com/2016/12/code-kata/)" 543 | ] 544 | }, 545 | { 546 | "cell_type": "markdown", 547 | "metadata": { 548 | "deletable": true, 549 | "editable": true 550 | }, 551 | "source": [ 552 | "### 7. Sum, max, etc can take generators, dict.get, flatten list of lists" 553 | ] 554 | }, 555 | { 556 | "cell_type": "code", 557 | "execution_count": 18, 558 | "metadata": { 559 | "collapsed": false, 560 | "deletable": true, 561 | "editable": true 562 | }, 563 | "outputs": [], 564 | "source": [ 565 | "scrabble_scores = [(1, \"E A O I N R T L S U\"), (2, \"D G\"), (3, \"B C M P\"),\n", 566 | " (4, \"F H V W Y\"), (5, \"K\"), (8, \"J X\"), (10, \"Q Z\")]\n", 567 | "LETTER_SCORES = {letter: score for score, letters in scrabble_scores\n", 568 | " for letter in letters.split()}" 569 | ] 570 | }, 571 | { 572 | "cell_type": "code", 573 | "execution_count": 19, 574 | "metadata": { 575 | "collapsed": false, 576 | "deletable": true, 577 | "editable": true 578 | }, 579 | "outputs": [ 580 | { 581 | "data": { 582 | "text/plain": [ 583 | "27" 584 | ] 585 | }, 586 | "execution_count": 19, 587 | "metadata": {}, 588 | "output_type": "execute_result" 589 | } 590 | ], 591 | "source": [ 592 | "word = 'contextmanagers'\n", 593 | "sum(LETTER_SCORES.get(char.upper(), 0) for char in word)" 594 | ] 595 | }, 596 | { 597 | "cell_type": "markdown", 598 | "metadata": { 599 | "deletable": true, 600 | "editable": true 601 | }, 602 | "source": [ 603 | "Another trick: flatten nested lists with sum:" 604 | ] 605 | }, 606 | { 607 | "cell_type": "code", 608 | "execution_count": 20, 609 | "metadata": { 610 | "collapsed": false, 611 | "deletable": true, 612 | "editable": true 613 | }, 614 | "outputs": [ 615 | { 616 | "data": { 617 | "text/plain": [ 618 | "[1, 2, 3, 4, 5, 6, 7, 8]" 619 | ] 620 | }, 621 | "execution_count": 20, 622 | "metadata": {}, 623 | "output_type": "execute_result" 624 | } 625 | ], 626 | "source": [ 627 | "sum([[1, 2], [3], [4, 5], [6, 7, 8]], []) " 628 | ] 629 | }, 630 | { 631 | "cell_type": "markdown", 632 | "metadata": { 633 | "deletable": true, 634 | "editable": true 635 | }, 636 | "source": [ 637 | "References: [PCC01](http://pybit.es/codechallenge01_review.html), [Daily Python Tip](https://twitter.com/python_tip/status/838705722779107328)" 638 | ] 639 | }, 640 | { 641 | "cell_type": "markdown", 642 | "metadata": { 643 | "deletable": true, 644 | "editable": true 645 | }, 646 | "source": [ 647 | "### 8. String formatting" 648 | ] 649 | }, 650 | { 651 | "cell_type": "code", 652 | "execution_count": 21, 653 | "metadata": { 654 | "collapsed": true, 655 | "deletable": true, 656 | "editable": true 657 | }, 658 | "outputs": [], 659 | "source": [ 660 | "country = \"Australia\"\n", 661 | "level = 11" 662 | ] 663 | }, 664 | { 665 | "cell_type": "code", 666 | "execution_count": 22, 667 | "metadata": { 668 | "collapsed": false, 669 | "deletable": true, 670 | "editable": true 671 | }, 672 | "outputs": [ 673 | { 674 | "name": "stdout", 675 | "output_type": "stream", 676 | "text": [ 677 | "The awesomeness level of Australia is 11.\n", 678 | "The awesomeness level of Australia is 11.\n", 679 | "The awesomeness level of Australia is 11.\n" 680 | ] 681 | } 682 | ], 683 | "source": [ 684 | "# Beautiful is better than ugly.\n", 685 | "print(\"The awesomeness level of \" + country + \" is \" + str(level) + \".\")\n", 686 | "print(\"The awesomeness level of %s is %d.\" % (country, level))\n", 687 | "# much better: \n", 688 | "print(\"The awesomeness level of {} is {}.\".format(country, level))" 689 | ] 690 | }, 691 | { 692 | "cell_type": "code", 693 | "execution_count": 23, 694 | "metadata": { 695 | "collapsed": false, 696 | "deletable": true, 697 | "editable": true 698 | }, 699 | "outputs": [ 700 | { 701 | "data": { 702 | "text/plain": [ 703 | "'The awesomeness level of Australia is 11.'" 704 | ] 705 | }, 706 | "execution_count": 23, 707 | "metadata": {}, 708 | "output_type": "execute_result" 709 | } 710 | ], 711 | "source": [ 712 | "# py 3.6 == f-string!\n", 713 | "f\"The awesomeness level of {country} is {level}.\"" 714 | ] 715 | }, 716 | { 717 | "cell_type": "markdown", 718 | "metadata": { 719 | "deletable": true, 720 | "editable": true 721 | }, 722 | "source": [ 723 | "References: [Pythonic String Formatting](http://pybit.es/string-formatting.html)" 724 | ] 725 | }, 726 | { 727 | "cell_type": "markdown", 728 | "metadata": { 729 | "collapsed": true, 730 | "deletable": true, 731 | "editable": true 732 | }, 733 | "source": [ 734 | "### 9. Use join over string concatenation" 735 | ] 736 | }, 737 | { 738 | "cell_type": "code", 739 | "execution_count": 24, 740 | "metadata": { 741 | "collapsed": true, 742 | "deletable": true, 743 | "editable": true 744 | }, 745 | "outputs": [], 746 | "source": [ 747 | "def strip_non_ascii(w):\n", 748 | " return ''.join([i for i in w if i in ascii_lowercase])" 749 | ] 750 | }, 751 | { 752 | "cell_type": "code", 753 | "execution_count": 25, 754 | "metadata": { 755 | "collapsed": false, 756 | "deletable": true, 757 | "editable": true 758 | }, 759 | "outputs": [], 760 | "source": [ 761 | "stripped_tags = [strip_non_ascii(tag) for tag in tags]" 762 | ] 763 | }, 764 | { 765 | "cell_type": "code", 766 | "execution_count": 27, 767 | "metadata": { 768 | "collapsed": false, 769 | "deletable": true, 770 | "editable": true 771 | }, 772 | "outputs": [ 773 | { 774 | "name": "stdout", 775 | "output_type": "stream", 776 | "text": [ 777 | "Most common: python, code, learning, tips, tricks, challenges, github, data, cleancode, best\n", 778 | "Most common: python, code, learning, tips, tricks, challenges, github, data, cleancode, best\n" 779 | ] 780 | } 781 | ], 782 | "source": [ 783 | "# bad \n", 784 | "msg = 'Most common: '\n", 785 | "for tag, __ in most_common_tags: # __ = throw away variable\n", 786 | " msg += tag + ', '\n", 787 | "print(msg[:-2])\n", 788 | "\n", 789 | "# use join = cleaner and faster\n", 790 | "msg = 'Most common: '\n", 791 | "tags_str = ', '.join([tag for tag, _ in most_common_tags])\n", 792 | "print('{}{}'.format(msg, tags_str))" 793 | ] 794 | }, 795 | { 796 | "cell_type": "markdown", 797 | "metadata": { 798 | "deletable": true, 799 | "editable": true 800 | }, 801 | "source": [ 802 | "References: [PCC05](https://github.com/pybites/challenges/blob/solutions/05/similar_tweeters.py), [Faster Python](http://pybit.es/faster-python.html)" 803 | ] 804 | }, 805 | { 806 | "cell_type": "markdown", 807 | "metadata": { 808 | "deletable": true, 809 | "editable": true 810 | }, 811 | "source": [ 812 | "### 10. Using generators for performance + reduce complexity" 813 | ] 814 | }, 815 | { 816 | "cell_type": "markdown", 817 | "metadata": { 818 | "deletable": true, 819 | "editable": true 820 | }, 821 | "source": [ 822 | "Use generators for faster and cleaner code" 823 | ] 824 | }, 825 | { 826 | "cell_type": "code", 827 | "execution_count": 8, 828 | "metadata": { 829 | "collapsed": false, 830 | "deletable": true, 831 | "editable": true 832 | }, 833 | "outputs": [ 834 | { 835 | "name": "stdout", 836 | "output_type": "stream", 837 | "text": [ 838 | "192.168.0.1\n", 839 | "192.168.0.2\n", 840 | "192.168.0.3\n", 841 | "192.168.0.4\n", 842 | "192.168.0.5\n" 843 | ] 844 | } 845 | ], 846 | "source": [ 847 | "# generate a list of IP addresses\n", 848 | "def get_nodes(net='192.168.0'): \n", 849 | " for i in range(1, 256): \n", 850 | " yield '{}.{}'.format(net, i)\n", 851 | " \n", 852 | "# get the first 5\n", 853 | "for ip in list(get_nodes())[:5]:\n", 854 | " print(ip)" 855 | ] 856 | }, 857 | { 858 | "cell_type": "code", 859 | "execution_count": 7, 860 | "metadata": { 861 | "collapsed": false 862 | }, 863 | "outputs": [ 864 | { 865 | "name": "stdout", 866 | "output_type": "stream", 867 | "text": [ 868 | "4\n", 869 | "8\n", 870 | "16\n", 871 | "32\n", 872 | "64\n", 873 | "128\n" 874 | ] 875 | } 876 | ], 877 | "source": [ 878 | "# indefinitely double a given number\n", 879 | "def num_gen(num):\n", 880 | " while True:\n", 881 | " num += num\n", 882 | " yield num\n", 883 | "\n", 884 | "# don't materialize this one with list() !\n", 885 | "for i, num in enumerate(num_gen(2)):\n", 886 | " print(num)\n", 887 | " if i == 5:\n", 888 | " break" 889 | ] 890 | }, 891 | { 892 | "cell_type": "markdown", 893 | "metadata": { 894 | "deletable": true, 895 | "editable": true 896 | }, 897 | "source": [ 898 | "References: [PCC02](https://github.com/pybites/challenges/blob/solutions/02/game.py), [PCC03](https://github.com/pybites/challenges/blob/solutions/03/tags.py), [PCC07](https://github.com/pybites/challenges/blob/solutions/07/sentiment.py), [PCC09](https://github.com/pybites/challenges/blob/solutions/09/with_ssh.py), [Faster Python](http://pybit.es/faster-python.html)" 899 | ] 900 | }, 901 | { 902 | "cell_type": "markdown", 903 | "metadata": { 904 | "deletable": true, 905 | "editable": true 906 | }, 907 | "source": [ 908 | "### Bonus: write Powerful Python using dunder (magic) methods" 909 | ] 910 | }, 911 | { 912 | "cell_type": "markdown", 913 | "metadata": { 914 | "deletable": true, 915 | "editable": true 916 | }, 917 | "source": [ 918 | "See primer post [here](http://pybit.es/python-data-model.html)" 919 | ] 920 | } 921 | ], 922 | "metadata": { 923 | "kernelspec": { 924 | "display_name": "venv", 925 | "language": "python", 926 | "name": "venv" 927 | }, 928 | "language_info": { 929 | "codemirror_mode": { 930 | "name": "ipython", 931 | "version": 3 932 | }, 933 | "file_extension": ".py", 934 | "mimetype": "text/x-python", 935 | "name": "python", 936 | "nbconvert_exporter": "python", 937 | "pygments_lexer": "ipython3", 938 | "version": "3.6.0" 939 | } 940 | }, 941 | "nbformat": 4, 942 | "nbformat_minor": 0 943 | } 944 | -------------------------------------------------------------------------------- /notebooks/tags.txt: -------------------------------------------------------------------------------- 1 | python 2 | tips 3 | tricks 4 | resources 5 | flask 6 | cron 7 | tools 8 | scrabble 9 | code challenges 10 | github 11 | fork 12 | learning 13 | game 14 | itertools 15 | random 16 | sets 17 | twitter 18 | news 19 | python 20 | podcasts 21 | data science 22 | challenges 23 | apis 24 | conda 25 | 3.6 26 | code challenges 27 | code review 28 | hn 29 | github 30 | learning 31 | max 32 | generators 33 | scrabble 34 | refactoring 35 | iterators 36 | itertools 37 | tricks 38 | generator 39 | games 40 | notebooks 41 | permutations 42 | python 43 | tips 44 | tricks 45 | code 46 | pybites 47 | beautifulsoup 48 | bs4 49 | webscraping 50 | namedtuple 51 | pythonic 52 | cleancode 53 | collections 54 | 2vs3 55 | namedtuple 56 | decorators 57 | contextmanagers 58 | scrabble 59 | tdd 60 | code challenges 61 | github 62 | learning 63 | twitter 64 | news 65 | python 66 | podcasts 67 | data 68 | iterators 69 | pythontips 70 | python 71 | tips 72 | tricks 73 | code 74 | pybites 75 | challenge 76 | refactoring 77 | code review 78 | best practices 79 | pythonic 80 | git 81 | github 82 | git flow 83 | vim 84 | assert 85 | challenges 86 | learning 87 | python 88 | beginners 89 | code 90 | algorithms 91 | data structures 92 | performance 93 | collections 94 | pep8 95 | cleancode 96 | guidelines 97 | coding style 98 | best practices 99 | pythonic 100 | vim 101 | learning 102 | python 103 | beginners 104 | tips 105 | cleancode 106 | best practices 107 | 3.6 108 | features 109 | release 110 | asyncio 111 | formatting 112 | typing 113 | dicts 114 | secrets 115 | generators 116 | readability 117 | python 118 | learning 119 | beginners 120 | tips 121 | cleancode 122 | best practices 123 | twitterapi 124 | tweepy 125 | feedparser 126 | rss 127 | logging 128 | podcasts 129 | virtualenv 130 | pyvenv 131 | venv 132 | news 133 | 3.6 134 | best practices 135 | pep8 136 | virtualenv 137 | cleancode 138 | logging 139 | pytest 140 | ebook 141 | refactoring 142 | gotchas 143 | kindle 144 | template strings 145 | json 146 | html 147 | books 148 | bookcision 149 | generators 150 | python 151 | review 152 | books 153 | learning 154 | beginners 155 | automation 156 | zip 157 | packaging 158 | distribute 159 | pip 160 | pelican 161 | feedparser 162 | rss 163 | pythonic 164 | books 165 | collections 166 | tricks 167 | tips 168 | data science 169 | matplotlib 170 | pandas 171 | python 172 | pip 173 | virtualenv 174 | venv 175 | collections 176 | data structures 177 | performance 178 | stdlib 179 | deque 180 | pelican 181 | publishing 182 | github 183 | pip 184 | virtualenv 185 | git 186 | pelican 187 | publishing 188 | blog 189 | pybites -------------------------------------------------------------------------------- /packt/packt.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from collections import namedtuple 3 | import os 4 | 5 | from selenium import webdriver 6 | import requests 7 | import tweepy 8 | from selenium.webdriver.chrome.options import Options 9 | 10 | 11 | PACKT_FREE_LEARNING = "https://www.packtpub.com/packt/offers/free-learning" 12 | HELP_TEXT = 'Packt free book (video) of the day' 13 | UPDATE_MSG = """Packt Free Learning of the day: 14 | {title} 15 | by {author} (published: {pub_date}) 16 | {link} 17 | 18 | Expires in {expires} ... grab it now! 19 | 20 | {cover} 21 | """ 22 | CONSUMER_KEY = os.environ['CONSUMER_KEY'] 23 | CONSUMER_SECRET = os.environ['CONSUMER_SECRET'] 24 | ACCESS_TOKEN = os.environ['ACCESS_TOKEN'] 25 | ACCESS_SECRET = os.environ['ACCESS_SECRET'] 26 | SLACK_WEBHOOK_URL = '' 27 | 28 | Book = namedtuple('Book', 'title author pub_date cover expires') 29 | 30 | 31 | def _create_update(book): 32 | return UPDATE_MSG.format(title=book.title, 33 | author=book.author, 34 | pub_date=book.pub_date, 35 | link=PACKT_FREE_LEARNING, 36 | expires=book.expires, 37 | cover=book.cover) 38 | 39 | 40 | def get_packt_book(): 41 | options = Options() 42 | options.add_argument("--headless") 43 | 44 | driver = webdriver.Chrome(options=options) 45 | driver.get(PACKT_FREE_LEARNING) 46 | 47 | find_class = driver.find_element_by_class_name 48 | title = find_class('product__title').text 49 | author = find_class('product__author').text 50 | pub_date = find_class('product__publication-date').text 51 | cover = find_class('product__img').get_attribute("src") 52 | 53 | timer = find_class('countdown__title').text 54 | hours = timer.split()[-1].split(':')[0] 55 | expires = f'in {hours} hours' 56 | 57 | driver.quit() 58 | 59 | book = Book(title, author, pub_date, cover, expires) 60 | update = _create_update(book) 61 | return update 62 | 63 | 64 | def twitter_authenticate(): 65 | auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET) 66 | auth.set_access_token(ACCESS_TOKEN, ACCESS_SECRET) 67 | return tweepy.API(auth) 68 | 69 | 70 | def post_to_twitter(book_post): 71 | try: 72 | api = twitter_authenticate() 73 | api.update_status(book_post) 74 | print(f'Shared title on Twitter') 75 | except Exception as exc: 76 | print(f'Error posting to Twitter - exception: {exc}') 77 | 78 | 79 | def post_to_slack(book_post): 80 | try: 81 | resp = requests.post(SLACK_WEBHOOK_URL, 82 | json={'text': book_post}) 83 | import pdb; pdb.set_trace() 84 | if resp.status_code == 201: 85 | print(f'Shared title on Slack') 86 | else: 87 | raise 88 | except Exception as exc: 89 | print(f'Error posting to Slack - exception: {exc}') 90 | 91 | 92 | if __name__ == '__main__': 93 | parser = argparse.ArgumentParser(description=HELP_TEXT) 94 | parser.add_argument('-t', '--twitter', action='store_true', 95 | help="Post title to Twitter") 96 | parser.add_argument('-s', '--slack', action='store_true', 97 | help="Post title to Slack") 98 | args = parser.parse_args() 99 | 100 | book_update = get_packt_book() 101 | print(book_update) 102 | 103 | if args.slack: 104 | post_to_twitter(book_update) 105 | 106 | if args.twitter: 107 | post_to_slack(book_update) 108 | -------------------------------------------------------------------------------- /pillow/banner/.gitignore: -------------------------------------------------------------------------------- 1 | out.png 2 | -------------------------------------------------------------------------------- /pillow/banner/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybites/blog_code/902ebb87e5f7a407714d0e399833f0331a1b915d/pillow/banner/__init__.py -------------------------------------------------------------------------------- /pillow/banner/assets/SourceSansPro-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybites/blog_code/902ebb87e5f7a407714d0e399833f0331a1b915d/pillow/banner/assets/SourceSansPro-Regular.otf -------------------------------------------------------------------------------- /pillow/banner/assets/Ubuntu-R.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybites/blog_code/902ebb87e5f7a407714d0e399833f0331a1b915d/pillow/banner/assets/Ubuntu-R.ttf -------------------------------------------------------------------------------- /pillow/banner/assets/pillow-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybites/blog_code/902ebb87e5f7a407714d0e399833f0331a1b915d/pillow/banner/assets/pillow-logo.png -------------------------------------------------------------------------------- /pillow/banner/assets/pybites-challenges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybites/blog_code/902ebb87e5f7a407714d0e399833f0331a1b915d/pillow/banner/assets/pybites-challenges.png -------------------------------------------------------------------------------- /pillow/banner/banner.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from pathlib import Path 3 | import sys 4 | 5 | from PIL import Image, ImageDraw, ImageFont 6 | 7 | ASSET_DIR = 'assets' 8 | DEFAULT_WIDTH = 600 9 | DEFAULT_HEIGHT = 150 10 | DEFAULT_CANVAS_SIZE = (DEFAULT_WIDTH, DEFAULT_HEIGHT) 11 | DEFAULT_OUTPUT_FILE = 'out.png' 12 | RESIZE_PERCENTAGE = 0.8 13 | DEFAULT_TOP_MARGIN = int(((1 - 0.8) * DEFAULT_HEIGHT) / 2) 14 | WHITE, BLACK = (255, 255, 255), (0, 0, 0) 15 | TEXT_SIZE = 24 16 | TEXT_FONT_TYPE = Path(ASSET_DIR, 'SourceSansPro-Regular.otf') 17 | TEXT_PADDING_HOR, TEXT_PADDING_VERT = 20, 40 18 | 19 | Font = namedtuple('Font', 'ttf text color size offset') 20 | ImageDetails = namedtuple('Image', 'left top size') 21 | 22 | 23 | class Banner: 24 | def __init__(self, size=DEFAULT_CANVAS_SIZE, 25 | bgcolor=WHITE, output_file=DEFAULT_OUTPUT_FILE): 26 | '''Creating a new canvas''' 27 | self.size = size 28 | self.bgcolor = bgcolor 29 | self.output_file = output_file 30 | self.image = Image.new('RGBA', self.size, self.bgcolor) 31 | self.image_coords = [] 32 | 33 | def _image_gt_canvas_size(self, img): 34 | return img.size[0] > self.image.size[0] or \ 35 | img.size[1] > self.image.size[1] 36 | 37 | def add_image(self, image, resize=False, 38 | top=DEFAULT_TOP_MARGIN, left=0, right=False): 39 | '''Adds (pastes) image on canvas 40 | If right is given calculate left, else take left 41 | Returns added img size''' 42 | img = Image.open(image) 43 | 44 | if resize or self._image_gt_canvas_size(img): 45 | size = DEFAULT_HEIGHT * RESIZE_PERCENTAGE 46 | img.thumbnail((size, size), Image.ANTIALIAS) 47 | 48 | if right: 49 | left = self.image.size[0] - img.size[0] 50 | 51 | offset = (left, top) 52 | self.image.paste(img, offset) 53 | 54 | img_details = ImageDetails(left=left, top=top, size=img.size) 55 | self.image_coords.append(img_details) 56 | 57 | def add_text(self, font): 58 | '''Adds text on a given image object''' 59 | draw = ImageDraw.Draw(self.image) 60 | pillow_font = ImageFont.truetype(font.ttf, font.size) 61 | 62 | if font.offset: 63 | offset = font.offset 64 | else: 65 | # if no offset given put text alongside first image 66 | left_image_px = min(img.left + img.size[0] 67 | for img in self.image_coords) 68 | offset = (left_image_px + TEXT_PADDING_HOR, 69 | TEXT_PADDING_VERT) 70 | 71 | draw.text(offset, font.text, font.color, font=pillow_font) 72 | 73 | def save_image(self): 74 | self.image.save(self.output_file) 75 | 76 | 77 | def main(args): 78 | image1 = args[0] 79 | image2 = args[1] 80 | text = args[2] 81 | 82 | banner = Banner() 83 | banner.add_image(image1) 84 | banner.add_image(image2, resize=True, right=True) 85 | 86 | font = Font(ttf=TEXT_FONT_TYPE, 87 | text=text, 88 | color=BLACK, 89 | size=TEXT_SIZE, 90 | offset=None) 91 | 92 | banner.add_text(font) 93 | 94 | banner.save_image() 95 | 96 | 97 | if __name__ == '__main__': 98 | script = sys.argv.pop(0) 99 | args = sys.argv 100 | 101 | if len(args) != 3: 102 | print('Usage: {} img1 img2 text'.format(script)) 103 | sys.exit(1) 104 | 105 | main(args) 106 | -------------------------------------------------------------------------------- /pillow/requirements.txt: -------------------------------------------------------------------------------- 1 | olefile==0.44 2 | Pillow==4.2.1 3 | -------------------------------------------------------------------------------- /pybites_digest/README.md: -------------------------------------------------------------------------------- 1 | Uses Python3 2 | 3 | pip install -r requirements.txt 4 | 5 | $ more pybites_header 6 | To: 7 | Subject: Weekly PyBites digest 8 | Content-Type: text/html 9 | 10 | Cronjob: 11 | 12 | $ cat pybites_header <(python3 /path/to/pybites_digest/digest.py) | sendmail -t 13 | -------------------------------------------------------------------------------- /pybites_digest/digest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | import time 5 | 6 | import feedparser 7 | 8 | DEFAULT_DAYS_BACK = 7 9 | FEEDS = 'http://pybit.es/feeds' 10 | RSS = 'all.rss.xml' 11 | SECS_IN_DAY = 24 * 60 * 60 12 | ARTICLE_HTML = '''
    13 |

    {}

    14 | {} 15 |
    ''' 16 | ARTICLE_TXT = '''{} 17 | {} 18 | {} 19 | -- 20 | ''' 21 | 22 | 23 | def calc_utstamp_limit(days_back): 24 | ''' calculates unix timesamp from now minus days back ''' 25 | now = int(time.time()) 26 | return now - (days_back * SECS_IN_DAY) 27 | 28 | 29 | def get_articles(utstamp_limit): 30 | xml = RSS if os.path.isfile(RSS) else os.path.join(FEEDS, RSS) 31 | feed = feedparser.parse(xml) 32 | for article in feed['entries']: 33 | utstamp = time_to_unix(article['published_parsed']) 34 | if utstamp < utstamp_limit: 35 | continue 36 | yield article 37 | 38 | 39 | def time_to_unix(t): 40 | return int(time.mktime(t)) 41 | 42 | 43 | def strip_html(text): 44 | return re.sub('<[^<]+?>', '', text) 45 | 46 | 47 | if __name__ == "__main__": 48 | days_back = int(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_DAYS_BACK 49 | report_html = True if len(sys.argv) > 2 else False 50 | utstamp_limit = calc_utstamp_limit(days_back) 51 | for article in get_articles(utstamp_limit): 52 | if report_html: 53 | print(ARTICLE_HTML.format( 54 | article['link'], article['title'], article['summary'])) 55 | else: 56 | html = strip_html(article['summary']) 57 | print(ARTICLE_TXT.format( 58 | article['title'], article['link'], html)) 59 | -------------------------------------------------------------------------------- /pybites_digest/requirements.txt: -------------------------------------------------------------------------------- 1 | feedparser 2 | -------------------------------------------------------------------------------- /pybites_review/prs.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | import re 3 | 4 | import requests 5 | 6 | GH_API_PULLS_ENDPOINT = 'https://api.github.com/repos/pybites/challenges/pulls' 7 | PR_LINK = "https://github.com/pybites/challenges/pull/{id}" 8 | CHALLENGE_LINK = "http://codechalleng.es/challenges/{id}" 9 | EXTRACT_TEMPLATE = re.compile(r'.*learn\?\):\s+\[(.*?)\]Other.*') 10 | 11 | 12 | def get_learning(template): 13 | """Helper to extract learning from PR template""" 14 | learning = ''.join(template.split('\r\n')) 15 | return EXTRACT_TEMPLATE.sub(r'\1', learning).strip() 16 | 17 | 18 | def get_open_prs(): 19 | """Parse GH API pulls JSON into a dict of keys = code challenge ids 20 | and values = lists of (pr_number, learning) tuples""" 21 | open_pulls = requests.get(GH_API_PULLS_ENDPOINT).json() 22 | prs = defaultdict(list) 23 | 24 | for pull in open_pulls: 25 | pr_number = pull['number'] 26 | 27 | pcc = pull['head']['ref'].upper() 28 | learning = get_learning(pull['body']) 29 | if learning: 30 | prs[pcc].append((pr_number, learning)) 31 | 32 | return prs 33 | 34 | 35 | def print_review_markdown(prs): 36 | """Return markdown for review post, e.g. 37 | https://pybit.es/codechallenge57_review.html -> 38 | Read Code for Fun and Profit""" 39 | for pcc, prs in sorted(prs.items()): 40 | challenge_link = CHALLENGE_LINK.format(id=pcc.strip('PCC')) 41 | print(f'\n#### [{pcc}]({challenge_link})') 42 | 43 | for i, (pr_number, learning) in enumerate(prs): 44 | if i > 0: 45 | print('\n') 46 | pr_link = PR_LINK.format(id=pr_number) 47 | print(f'\n> {learning} - [PR]({pr_link})') 48 | 49 | 50 | if __name__ == '__main__': 51 | prs = get_open_prs() 52 | print_review_markdown(prs) 53 | -------------------------------------------------------------------------------- /pybites_review/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /pytest/fixtures/.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .*cache* 3 | -------------------------------------------------------------------------------- /pytest/fixtures/conftest.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | import pytest 4 | 5 | from groceries import Groceries, Item 6 | 7 | 8 | @pytest.fixture(scope="session") 9 | def cart(): 10 | """Setup code to create a groceries cart object with 6 items in it""" 11 | print('sleeping a bit at session level') 12 | sleep(1) # for scope=module/session demo purposes 13 | products = 'celery apples water coffee chicken pizza'.split() 14 | prices = [1, 4, 2, 5, 6, 4] 15 | cravings = False, False, False, False, False, True 16 | 17 | items = [] 18 | for item in zip(products, prices, cravings): 19 | items.append(Item(*item)) 20 | 21 | return Groceries(items) 22 | -------------------------------------------------------------------------------- /pytest/fixtures/groceries.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | MAX_CRAVINGS = 2 4 | 5 | Item = namedtuple('Item', 'product price craving') 6 | 7 | 8 | class DuplicateProduct(Exception): 9 | pass 10 | 11 | 12 | class MaxCravingsReached(Exception): 13 | pass 14 | 15 | 16 | class Groceries: 17 | 18 | def __init__(self, items=None): 19 | """This cart can be instantiated with a list of namedtuple 20 | items, if not provided use an empty list""" 21 | self._items = items if items is not None else [] 22 | 23 | def show(self): 24 | """Print a simple table of cart items with total at the end""" 25 | for item in self._items: 26 | product = f'{item.product}' 27 | if item.craving: 28 | product += ' (craving)' 29 | print(f'{product:<30} | {item.price:>3}') 30 | print('-' * 36) 31 | print(f'{"Total":<30} | {self.due:>3}') 32 | 33 | def add(self, new_item): 34 | """Add a new item to cart, raise exceptions if item already in 35 | cart, or when we exceed MAX_CRAVINGS""" 36 | if any(item for item in self if item.product == new_item.product): 37 | raise DuplicateProduct(f'{new_item.product} already in items') 38 | if new_item.craving and self.num_cravings_reached: 39 | raise MaxCravingsReached(f'{MAX_CRAVINGS} allowed') 40 | self._items.append(new_item) 41 | 42 | def delete(self, product): 43 | """Delete item matching 'product', raises IndexError 44 | if no item matches""" 45 | for i, item in enumerate(self): 46 | if item.product == product: 47 | self._items.pop(i) 48 | break 49 | else: 50 | raise IndexError(f'{product} not in cart') 51 | 52 | def search(self, search): 53 | """Case insensitive 'contains' search, this is a 54 | generator returning matching Item namedtuples""" 55 | for item in self: 56 | if search.lower() in item.product: 57 | yield item 58 | 59 | @property 60 | def due(self): 61 | """Calculate total due value of cart""" 62 | return sum(item.price for item in self) 63 | 64 | @property 65 | def num_cravings_reached(self): 66 | """Checks if I have too many cravings in my cart """ 67 | return len([item for item in self if item.craving]) >= MAX_CRAVINGS 68 | 69 | def __len__(self): 70 | """The len of cart""" 71 | return len(self._items) 72 | 73 | def __getitem__(self, index): 74 | """Making the class iterable (cart = Groceries() -> cart[1] etc) 75 | without this dunder I would get 'TypeError: 'Cart' object does 76 | not support indexing' when trying to index it""" 77 | return self._items[index] 78 | -------------------------------------------------------------------------------- /pytest/fixtures/requirements.txt: -------------------------------------------------------------------------------- 1 | attrs==17.4.0 2 | coverage==4.5.1 3 | pluggy==0.6.0 4 | py==1.5.2 5 | pytest==3.4.2 6 | pytest-cov==2.5.1 7 | six==1.11.0 8 | -------------------------------------------------------------------------------- /pytest/fixtures/test_edit_cart.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | import pytest 4 | 5 | from groceries import Item, DuplicateProduct, MaxCravingsReached 6 | 7 | 8 | def test_add_item(cart): 9 | cart = deepcopy(cart) # not needed if scope=function 10 | 11 | oranges = Item(product='oranges', price=3, craving=False) 12 | cart.add(oranges) 13 | 14 | assert len(cart) == 7 15 | assert cart[-1].product == 'oranges' 16 | assert cart[-1].price == 3 17 | assert cart.due == 25 18 | assert not cart.num_cravings_reached 19 | 20 | 21 | def test_add_item_max_cravings(cart): 22 | cart = deepcopy(cart) 23 | chocolate = Item(product='chocolate', price=2, craving=True) 24 | cart.add(chocolate) 25 | assert cart.num_cravings_reached 26 | 27 | croissants = Item(product='croissants', price=3, craving=True) 28 | with pytest.raises(MaxCravingsReached): 29 | cart.add(croissants) # wait till next week! 30 | 31 | 32 | def test_add_item_duplicate(cart): 33 | apples = Item(product='apples', price=4, craving=False) 34 | with pytest.raises(DuplicateProduct): 35 | cart.add(apples) 36 | 37 | 38 | def test_delete_item(cart): 39 | cart = deepcopy(cart) 40 | # not in collection 41 | croissant = 'croissant' 42 | with pytest.raises(IndexError): 43 | cart.delete(croissant) 44 | 45 | # in collection 46 | assert len(cart) == 6 47 | apples = 'apples' 48 | cart.delete(apples) 49 | # new product at this index 50 | assert len(cart) == 5 51 | assert cart[1].product == 'water' 52 | -------------------------------------------------------------------------------- /pytest/fixtures/test_view_cart.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import pytest 4 | 5 | from groceries import Groceries 6 | 7 | 8 | def test_initial_empty_cart(): 9 | """Note no fixture here to test an empty cart creation""" 10 | cart = Groceries() 11 | assert len(cart) == 0 12 | assert cart.due == 0 13 | 14 | 15 | def test_initial_filled_cart(cart): 16 | # thanks to __getitem__ can index the cart 17 | assert cart[0].product == 'celery' 18 | assert cart[0].price == 1 19 | assert cart[-1].product == 'pizza' 20 | assert cart[-1].price == 4 21 | 22 | assert len(cart) == 6 23 | assert cart.due == 22 24 | assert not cart.num_cravings_reached 25 | 26 | 27 | @pytest.mark.parametrize("test_input,expected", [ 28 | ('banana', 0), 29 | ('water', 1), 30 | ('Apples', 1), 31 | ('apple', 1), 32 | ('le', 2), 33 | ('zZ', 1), 34 | ('e', 5), 35 | ]) 36 | def test_search_item(cart, test_input, expected): 37 | assert len(list(cart.search(test_input))) == expected 38 | 39 | 40 | def test_show_items(cart, capfd): 41 | cart.show() 42 | output = [line for line in capfd.readouterr()[0].split('\n') 43 | if line.strip()] 44 | 45 | assert re.search(r'^celery.*1$', output[0]) 46 | assert re.search(r'^pizza \(craving\).*4$', output[5]) 47 | assert re.search(r'^Total.*22$', output[-1]) 48 | -------------------------------------------------------------------------------- /selenium/README.md: -------------------------------------------------------------------------------- 1 | ## Selenium starter code 2 | 3 | Selenium + pytest (not Django) code from our article: [How to Test Your Django App with Selenium and pytest](https://pybit.es/selenium-pytest-and-django.html). 4 | 5 | Required setup: 6 | 7 | 1. Virtual env: `python3 -m venv venv` 8 | 9 | 2. Dependencies: `pip install -r requirements.txt` 10 | 11 | 3. Store your Github user as env variables in your `venv/bin/activate` script: 12 | 13 | export USER_NAME= 14 | export USER_PASSWORD= 15 | 16 | 4. Activate your venv: `source venv/bin/activate` 17 | 18 | ### Have fun! 19 | 20 | And now go implement your own end-to-end testing, have fun! 21 | -------------------------------------------------------------------------------- /selenium/requirements.txt: -------------------------------------------------------------------------------- 1 | python-dateutil 2 | pytest 3 | selenium 4 | -------------------------------------------------------------------------------- /selenium/test.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | import os 3 | 4 | from dateutil.relativedelta import relativedelta 5 | import pytest 6 | from selenium import webdriver 7 | from selenium.webdriver.common.keys import Keys 8 | 9 | HOMEPAGE = "https://codechalleng.es" 10 | TODAY = date.today() 11 | 12 | USER_NAME = os.environ['USER_NAME'] 13 | USER_PASSWORD = os.environ['USER_PASSWORD'] 14 | 15 | 16 | def _make_3char_monthname(dt): 17 | return dt.strftime('%b').upper() 18 | 19 | 20 | @pytest.fixture(scope="module") 21 | def driver(): 22 | driver = webdriver.Chrome() 23 | yield driver 24 | driver.quit() 25 | 26 | 27 | def test_loggedout_homepage(driver): 28 | driver.get(HOMEPAGE) 29 | expected = "PyBites Code Challenges | Hone Your Python Skills" 30 | assert driver.title == expected 31 | 32 | 33 | def test_loggedin_dashboard(driver): 34 | driver.get(HOMEPAGE) 35 | driver.find_element_by_class_name('ghLoginBtn').click() 36 | driver.find_element_by_name('login').send_keys(USER_NAME) 37 | driver.find_element_by_name('password').send_keys(USER_PASSWORD + 38 | Keys.RETURN) 39 | 40 | h2s = [h2.text for h2 in driver.find_elements_by_tag_name('h2')] 41 | expected = [f'Happy Coding, {USER_NAME}!', 42 | 'PyBites Platform Updates [all]', 43 | 'PyBites Ninjas (score ≥ 50)', 44 | 'Become a better Pythonista!', 45 | 'Keep Calm and Code in Python! SHARE ON TWITTER'] 46 | for header in expected: 47 | assert header in h2s, f'{header} not in h2 headers' 48 | 49 | # calendar / coding streak feature 50 | this_month = _make_3char_monthname(TODAY) 51 | last_month = _make_3char_monthname(TODAY-relativedelta(months=+1)) 52 | two_months_ago = _make_3char_monthname(TODAY-relativedelta(months=+2)) 53 | for month in (this_month, last_month, two_months_ago): 54 | month_year = f'{month} {TODAY.year}' 55 | assert month_year in h2s, f'{month_year} not in h2 headers' 56 | 57 | # only current date is marked active 58 | assert len(driver.find_elements_by_class_name('today')) == 1 59 | -------------------------------------------------------------------------------- /steam_notifier/email_list.py: -------------------------------------------------------------------------------- 1 | EMAILS = ('your-email@gmail.com') 2 | -------------------------------------------------------------------------------- /steam_notifier/emailer.py: -------------------------------------------------------------------------------- 1 | #!python3 2 | #emailer.py is a simple script for sending emails using smtplib 3 | 4 | import smtplib 5 | import sqlite3 6 | from email.mime.multipart import MIMEMultipart 7 | from email.mime.text import MIMEText 8 | 9 | from email_list import EMAILS 10 | 11 | DATA_FILE = 'steam_games.db' 12 | from_addr = 'your-email@gmail.com' 13 | to_addr = EMAILS 14 | msg = MIMEMultipart() 15 | msg['From'] = from_addr 16 | msg['To'] = ", ".join(to_addr) 17 | msg['Subject'] = 'New Releases and Sales on Steam' 18 | 19 | body = '' 20 | 21 | with sqlite3.connect(DATA_FILE) as connection: 22 | c = connection.cursor() 23 | c.execute("SELECT Name, Link FROM new_steam_games WHERE Emailed='0'") 24 | for item in c.fetchall(): 25 | body += item[0] + ': ' + item[1] + '\n' 26 | c.execute("UPDATE new_steam_games SET Emailed='1'") 27 | 28 | msg.attach(MIMEText(body, 'plain')) 29 | 30 | smtp_server = smtplib.SMTP('smtp.gmail.com', 587) #Specify Gmail Mail server 31 | 32 | smtp_server.ehlo() #Send mandatory 'hello' message to SMTP server 33 | 34 | smtp_server.starttls() #Start TLS Encryption as we're not using SSL. 35 | 36 | #Login to gmail: Account | Password 37 | smtp_server.login(' your-email@gmail.com ', ' GMAIL-APP-ID-HERE ') 38 | 39 | text = msg.as_string() 40 | 41 | #Compile email list: From, To, Email body 42 | smtp_server.sendmail(from_addr, to_addr, text) 43 | 44 | #Close connection to SMTP server 45 | smtp_server.quit() 46 | 47 | #Test Message to verify all passes 48 | print('Email sent successfully') 49 | -------------------------------------------------------------------------------- /steam_notifier/pull_xml.py: -------------------------------------------------------------------------------- 1 | #!python3 2 | #pull_xml.py uses the requests module to pull down the feed xml file for use in the xml parser script. 3 | #This will result in just one call/request to the Steam webserver hosting this XML file. 4 | 5 | import requests 6 | 7 | URL = "http://store.steampowered.com/feeds/newreleases.xml" 8 | 9 | if __name__ == "__main__": 10 | r = requests.get(URL) 11 | with open('newreleases.xml', 'wb') as f: 12 | f.write(r.content) 13 | -------------------------------------------------------------------------------- /steam_notifier/readme.txt: -------------------------------------------------------------------------------- 1 | XML Steam Scraper 2 | 3 | A program to parse the steam XML feed for new titles. 4 | 5 | 1. Run pull_xml.py to pull down the steam newreleases XML feed. 6 | 7 | 2. Run xml_steam_scraper.py to parse the feed and save the game names and URLs to an sqlite database. (The db will be created on first run). 8 | 9 | 3. Populate email_list.py with email addresses. 10 | 11 | 4. Run emailer.py to send yourself the list of newly released games. 12 | 13 | 5. Automate this by adding each of the scripts (pull, scraper and emailer) to crontab in order. I'd recommend leaving a minute or two between each script run. 14 | 15 | 6. Set cron to run the 3 entries every day. The end result will be a daily email of the latest games as they're added to Steam. 16 | 17 | 7. The program is configured such that once a game has been emailed to you, it won't be emailed again. 18 | 19 | 20 | CRONTAB ENTRY EXAMPLE TO RUN SCRIPT AT 8:30PM DAILY: 21 | 22 | 30 20 * * * cd /opt/development/steamscraper && /usr/bin/python3 pull_xml.py 23 | -------------------------------------------------------------------------------- /steam_notifier/requirements.txt: -------------------------------------------------------------------------------- 1 | feedparser 2 | requests 3 | -------------------------------------------------------------------------------- /steam_notifier/xml_steam_scraper.py: -------------------------------------------------------------------------------- 1 | #!python3 2 | # steam_scraper.py is a simple web scraper to check for the latest steam games 3 | 4 | from collections import namedtuple 5 | import requests 6 | import sqlite3 7 | 8 | import feedparser 9 | 10 | FEED_FILE = "newreleases.xml" 11 | DB_NAME = "steam_games.db" 12 | Game = namedtuple('Game', 'title url') 13 | 14 | 15 | def check_create_db(): 16 | with sqlite3.connect(DB_NAME) as connection: 17 | c = connection.cursor() 18 | try: 19 | c.execute("""CREATE TABLE new_steam_games 20 | (Name TEXT, Link TEXT, Emailed TEXT) 21 | """) 22 | except: 23 | pass 24 | 25 | 26 | def pull_db_data(): 27 | db_games_list = [] 28 | with sqlite3.connect(DB_NAME) as connection: 29 | c = connection.cursor() 30 | c.execute("SELECT Name from new_steam_games") 31 | db_games_list = c.fetchall() 32 | return db_games_list 33 | 34 | 35 | def parse_that_feed_baby(): 36 | feed_list = [] 37 | feed = feedparser.parse(FEED_FILE) 38 | for entry in feed['entries']: 39 | game_data = Game(title=entry['title'], url=entry['link']) 40 | feed_list.append(game_data) 41 | return feed_list 42 | 43 | 44 | def check_for_new(feed_list, db_games): 45 | new_games_list = [] 46 | for data in feed_list: 47 | if (data.title,) not in db_games: 48 | new_games_list.append(data) 49 | return new_games_list 50 | 51 | 52 | def main(): 53 | check_create_db() 54 | db_games = pull_db_data() 55 | new_games = check_for_new(parse_that_feed_baby(), db_games) 56 | 57 | with sqlite3.connect(DB_NAME) as connection: 58 | c = connection.cursor() 59 | c.executemany("INSERT INTO new_steam_games VALUES (?, ?, 0)", new_games) 60 | 61 | if __name__ == "__main__": 62 | main() 63 | -------------------------------------------------------------------------------- /tips/requirements.txt: -------------------------------------------------------------------------------- 1 | bs4 2 | requests 3 | selenium # ~/bin/chromedriver 4 | -------------------------------------------------------------------------------- /tips/tips.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from random import choice 3 | import sys 4 | from time import sleep 5 | import urllib.parse 6 | 7 | import requests 8 | from selenium import webdriver 9 | 10 | TIPS_PAGE = 'https://codechalleng.es/api/tips' 11 | PYBITES_HAS_TWEETED = 'pybites/status' 12 | CARBON = 'https://carbon.now.sh/?l=python&code={code}' 13 | TWEET = '''{tip} {src} 14 | 15 | 🐍 Check out / submit more @pybites tips at https://codechalleng.es/tips 💡 16 | 17 | (image built with @carbon_app) 18 | 19 | {img} 20 | ''' 21 | 22 | Tip = namedtuple('Tip', 'tip code link') 23 | 24 | 25 | def retrieve_tips(): 26 | """Grab and parse all tips from https://codechalleng.es/tips 27 | returning a dict of keys: tip IDs and values: Tip namedtuples 28 | """ 29 | resp = requests.get(TIPS_PAGE) 30 | resp.raise_for_status() 31 | tips = {} 32 | 33 | for entry in resp.json(): 34 | # skip tips that were already shared 35 | code = entry['code'] 36 | if entry['share_link'] is not None or not code: 37 | continue 38 | 39 | idx = entry['id'] 40 | tip = entry['tip'] 41 | link = entry['link'] 42 | 43 | tips[idx] = Tip(tip=tip, code=code, link=link) 44 | 45 | return tips 46 | 47 | 48 | def get_carbon_image(tip): 49 | """Visit carbon.now.sh with the code, click the Tweet button 50 | and grab and return the Twitter picture url 51 | """ 52 | code = urllib.parse.quote_plus(tip.code) 53 | url = CARBON.format(code=code) 54 | 55 | driver = webdriver.Chrome() 56 | driver.get(url) 57 | 58 | driver.find_element_by_xpath("//button[contains(text(),'Tweet')]").click() 59 | sleep(10) # this might take a bit 60 | 61 | window_handles = driver.window_handles 62 | driver.switch_to.window(window_handles[1]) 63 | status = driver.find_element_by_id('status') 64 | img = status.text.split(' ')[-1] 65 | 66 | driver.quit() 67 | return img 68 | 69 | 70 | if __name__ == '__main__': 71 | tips = retrieve_tips() 72 | if len(sys.argv) == 2: 73 | tip_id = int(sys.argv[1]) 74 | else: 75 | tip_id = choice(list(tips.keys())) 76 | 77 | tip = tips.get(tip_id) 78 | if tip is None: 79 | print(f'Could not retrieve tip ID {tip_id}') 80 | sys.exit(1) 81 | 82 | src = f' - see {tip.link}' if tip.link else '' 83 | img = get_carbon_image(tip) 84 | 85 | tweet = TWEET.format(tip=tip.tip, src=src, img=img) 86 | # TODO: auto-post to twitter + POST link back to Tips API 87 | # but a bit of manual checking of the generated tweet is ok for now 88 | print(tweet) 89 | -------------------------------------------------------------------------------- /twitter_bot/.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | config.py 3 | *log 4 | .cache 5 | feeds 6 | -------------------------------------------------------------------------------- /twitter_bot/README.md: -------------------------------------------------------------------------------- 1 | ## README 2 | 3 | A bot to help us automate part of our [@pybites](https://twitter.com/pybites) posting. 4 | 5 | We use this in a daily cronjob and watch the following feeds: 6 | 7 | $ more feeds 8 | http://pybit.es/feeds/all.rss.xml 9 | https://talkpython.fm/episodes/rss 10 | https://pythonbytes.fm/episodes/rss 11 | https://dbader.org/rss 12 | https://www.codementor.io/python/tutorial/feed 13 | http://feeds.feedburner.com/PythonInsider 14 | http://www.weeklypython.chat/feed/ 15 | 16 | 17 | ## Use it for your own Twitter / feeds 18 | 19 | $ pyvenv venv 20 | $ source venv/bin/activate 21 | $ pip install -r requirements.txt 22 | $ mv config.py-example config.py (and edit it with your secret key/token etc) 23 | $ vi feeds (put your own feeds in) 24 | -------------------------------------------------------------------------------- /twitter_bot/config.py-example: -------------------------------------------------------------------------------- 1 | LOGFILE = "bot.log" 2 | HASHTAG = '#python' 3 | 4 | # twitter api 5 | CONSUMER_KEY = '' 6 | CONSUMER_SECRET = '' 7 | ACCESS_TOKEN = '' 8 | ACCESS_SECRET = '' 9 | -------------------------------------------------------------------------------- /twitter_bot/feeds-template: -------------------------------------------------------------------------------- 1 | http://planetpython.org/rss20.xml 2 | 3 | http://pybit.es/feeds/all.rss.xml 4 | https://talkpython.fm/episodes/rss 5 | https://pythonbytes.fm/episodes/rss 6 | https://dbader.org/rss 7 | https://www.codementor.io/python/tutorial/feed 8 | http://feeds.feedburner.com/PythonInsider 9 | http://www.weeklypython.chat/feed/ 10 | -------------------------------------------------------------------------------- /twitter_bot/requirements.txt: -------------------------------------------------------------------------------- 1 | feedparser 2 | requests 3 | requests-oauthlib 4 | six 5 | tweepy 6 | -------------------------------------------------------------------------------- /twitter_bot/tweetbot.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | import sys 4 | import time 5 | 6 | import feedparser 7 | import tweepy 8 | 9 | from config import LOGFILE, HASHTAG 10 | from config import CONSUMER_KEY, CONSUMER_SECRET 11 | from config import ACCESS_TOKEN, ACCESS_SECRET 12 | 13 | FEEDS = 'feeds' 14 | MAX_ENTRIES = 5 15 | NOW = time.localtime() 16 | NOW_UTSTAMP = time.mktime(NOW) 17 | NOW_READABLE = datetime.datetime.fromtimestamp( 18 | int(NOW_UTSTAMP) 19 | ).strftime('%Y-%m-%d %H:%M:%S') 20 | PYBITES = 'pybit.es' 21 | SECONDS_IN_DAY = 24 * 60 * 60 22 | 23 | logging.basicConfig(level=logging.DEBUG, 24 | format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', 25 | datefmt='%H:%M:%S', 26 | filename=LOGFILE, 27 | filemode='a') 28 | 29 | 30 | class TwitterApi(object): 31 | 32 | def __init__(self): 33 | auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET) 34 | auth.set_access_token(ACCESS_TOKEN, ACCESS_SECRET) 35 | self.api = tweepy.API(auth) 36 | logging.debug('Created {} instance'.format(self.__class__.__name__)) 37 | 38 | def post_tweet(self, status): 39 | try: 40 | self.api.update_status(status) 41 | logging.debug('posted status {} to twitter'.format(status)) 42 | except Exception as exc: 43 | logging.error('tweepy update_status error: {}'.format(exc)) 44 | 45 | 46 | def get_feeds(): 47 | with open(FEEDS) as f: 48 | return f.read().split() 49 | 50 | def within_last_day(tstamp): 51 | return (time.mktime(NOW) - time.mktime(tstamp)) / SECONDS_IN_DAY < 1 52 | 53 | def create_tweet(entry): 54 | return " ".join([entry['title'], entry['link'], HASHTAG]) 55 | 56 | def get_tweets(feed): 57 | for entry in feedparser.parse(feed)['entries'][:MAX_ENTRIES]: 58 | tstamp = entry['published_parsed'] 59 | if within_last_day(tstamp): 60 | yield create_tweet(entry) 61 | 62 | if __name__ == "__main__": 63 | logging.debug('New run at {}, processing feeds'.format(NOW_READABLE)) 64 | 65 | do_tweet = True 66 | if '-d' in sys.argv: 67 | do_tweet = False 68 | 69 | twapi = TwitterApi() 70 | for feed in get_feeds(): 71 | logging.debug('- feed: {}'.format(feed)) 72 | 73 | for tweet in get_tweets(feed): 74 | logging.debug(tweet) 75 | print(tweet) 76 | if do_tweet: 77 | twapi.post_tweet(tweet) 78 | -------------------------------------------------------------------------------- /various/harrypotter/harry.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | from string import punctuation 3 | import sys 4 | 5 | 6 | def strip_punctuation(word): 7 | '''Remove punctuation from a word''' 8 | return "".join(c for c in word if c not in punctuation) 9 | 10 | 11 | def get_words(text): 12 | '''Converts text into set of words without punctuation''' 13 | with open(text) as f: 14 | words = f.read().lower().split() 15 | words = [strip_punctuation(word) for word in words] 16 | # could remove stopwords but requires nltk.corpus 17 | return filter(None, words) 18 | 19 | 20 | def get_most_common(words, n=None): 21 | '''Return n common words, if n is None, return all (also singles)''' 22 | return Counter(words).most_common(n) 23 | 24 | 25 | if __name__ == "__main__": 26 | try: 27 | harry = sys.argv[1] 28 | except IndexError: 29 | harry = 'harry.txt' 30 | 31 | words = get_words(harry) 32 | common_words = get_most_common(words, n=20) 33 | 34 | for word, count in common_words: 35 | print('{:<4} {}'.format(count, word)) 36 | -------------------------------------------------------------------------------- /various/harrypotter/harry.txt: -------------------------------------------------------------------------------- 1 | The Boy Who Lived 2 | Mr. and Mrs. Dursley, of number four, Privet Drive, were proud to say that they were perfectly normal, thank you very much. They were the last people you'd expect to be involved in anything strange or mysterious, because they just didn't hold with such nonsense. 3 | 4 | Mr. Dursley was the director of a firm called Grunnings, which made drills. He was a big, beefy man with hardly any neck, although he did have a very large mustache. Mrs. Dursley was thin and blonde and had nearly twice the usual amount of neck, which came in very useful as she spent so much of her time craning over garden fences, spying on the neighbors. The Dursleys had a small son called Dudley and in their opinion there was no finer boy anywhere. 5 | 6 | The Dursleys had everything they wanted, but they also had a secret, and their greatest fear was that somebody would discover it. They didn't think they could bear it if anyone found out about the Potters. Mrs. Potter was Mrs. Dursley's sister, but they hadn't met for several years; in fact, Mrs. Dursley pretended she didn't have a sister, because her sister and her good-for-nothing husband were as unDursleyish as it was possible to be. The Dursleys shuddered to think what the neighbors would say if the Potters arrived in the street. The Dursleys knew that the Potters had a small son, too, but they had never even seen him. This boy was another good reason for keeping the Potters away; they didn't want Dudley mixing with a child like that. 7 | 8 | When Mr. and Mrs. Dursley woke up on the dull, gray Tuesday our story starts, there was nothing about the cloudy sky outside to suggest that strange and mysterious things would soon be happening all over the country. Mr. Dursley hummed as he picked out his most boring tie for work, and Mrs. Dursley gossiped away happily as she wrestled a screaming Dudley into his high chair. 9 | 10 | None of them noticed a large, tawny owl flutter past the window. 11 | 12 | At half past eight, Mr. Dursley picked up his briefcase, pecked Mrs. 13 | Dursley on the cheek, and tried to kiss Dudley good-bye but missed, because Dudley was now having a tantrum and throwing his cereal at the walls. 14 | 15 | "Little tyke," chortled Mr. Dursley as he left the house. He got into his car and backed out of number four's drive. 16 | 17 | It was on the corner of the street that he noticed the first sign of 18 | something peculiar -- a cat reading a map. For a second, Mr. Dursley didn't realize what he had seen -- then he jerked his head around to look again. There was a tabby cat standing on the corner of Privet Drive, but there wasn't a map in sight. What could he have been thinking of? It must have been a trick of the light. Mr. Dursley blinked and stared at the cat. It stared back. As Mr. Dursley drove around the corner and up the road, he watched the cat in his mirror. It was now reading the sign that said Privet Drive -- no, looking at the sign; cats couldn't read maps or signs. Mr. Dursley gave himself a little shake and put the cat out of his mind. As he drove toward town he thought of nothing except a large order of drills he was hoping to get that day. 19 | 20 | But on the edge of town, drills were driven out of his mind by something else. As he sat in the usual morning traffic jam, he couldn't help noticing that there seemed to be a lot of strangely dressed people about. People in cloaks. Mr. Dursley couldn't bear people who dressed in funny clothes -- the getups you saw on young people! He supposed this was some stupid new fashion. He drummed his fingers on the steering wheel and his eyes fell on a huddle of these weirdos standing quite close by. They were whispering excitedly together. Mr. Dursley was enraged to see that a couple of them weren't young at all; why, that man had to be older than he was, and wearing an emerald-green cloak! The nerve of him! But then it struck Mr. Dursley that this was probably some silly stunt -- these people were obviously collecting for something... yes, that would be it. The traffic moved on and a few minutes later, Mr. Dursley arrived in the Grunnings parking lot, his mind back on drills. 21 | 22 | Mr. Dursley always sat with his back to the window in his office on the ninth floor. If he hadn't, he might have found it harder to concentrate on drills that morning. He didn't see the owls swooping past in broad daylight, though people down in the street did; they pointed and gazed open- mouthed as owl after owl sped overhead. Most of them had never seen an owl even at nighttime. Mr. Dursley, however, had a perfectly normal, owl-free morning. He yelled at five different people. He made several important telephone calls and shouted a bit more. He was in a very good mood until lunchtime, when he thought he'd stretch his legs and walk across the road to buy himself a bun from the bakery. 23 | 24 | He'd forgotten all about the people in cloaks until he passed a group of them next to the baker's. He eyed them angrily as he passed. He didn't know why, but they made him uneasy. This bunch were whispering excitedly, too, and he couldn't see a single collecting tin. It was on his way back past them, clutching a large doughnut in a bag, that he caught a few words of what they were saying. 25 | 26 | "The Potters, that's right, that's what I heard -" 27 | 28 | "- yes, their son, Harry -" 29 | 30 | Mr. Dursley stopped dead. Fear flooded him. He looked back at the whisperers as if he wanted to say something to them, but thought better of it. 31 | He dashed back across the road, hurried up to his office, 32 | snapped at his secretary not to disturb him, seized his telephone, and had almost finished dialing his home number when he changed his mind. He put the receiver back down and stroked his mustache, thinking . . . no, he was being stupid. Potter wasn’t such an unusual name. He was sure there were lots of people called Potter who had a son called Harry. Come to think of it, he wasn’t even sure his nephew was called Harry. He’d never even seen the boy. It might have been Harvey. Or Harold. There was no point in worrying Mrs. Dursley; she always got so upset at any mention of her 33 | sister. He didn’t blame her — if he’d had a sister like that . . . but all the same, those people in cloaks . . . 34 | He found it a lot harder to concentrate on drills that afternoon 35 | and when he left the building at five o’clock, he was still so worried that he walked straight into someone just outside the door. 36 | 37 | "Sorry," he grunted, as the tiny old man stumbled and almost fell. It 38 | was a few seconds before Mr. Dursley realized that the man was wearing a violet cloak. He didn't seem at all upset at being almost knocked to the ground. On the contrary, his face split into a wide smile and he said in a squeaky voice that made passersby stare, "Don't be sorry, my dear sir, for nothing could upset me today! Rejoice, for You-Know-Who has gone at last! Even Muggles like yourself should be celebrating, this happy, happy day!" 39 | 40 | And the old man hugged Mr. Dursley around the middle and walked off. 41 | 42 | Mr. Dursley stood rooted to the spot. He had been hugged by a complete stranger. He also thought he had been called a Muggle, whatever that was. He was rattled. He hurried to his car and set off for home, hoping he was imagining things, which he had never hoped before, because he didn't approve of imagination. 43 | 44 | As he pulled into the driveway of number four, the first thing he saw -- and it didn't improve his mood -- was the tabby cat he'd spotted that morning. It was now sitting on his garden wall. He was sure it was the same one; it had the same markings around its eyes. 45 | 46 | "Shoo!" said Mr. Dursley loudly. 47 | 48 | The cat didn't move. It just gave him a stern look. Was this normal cat behavior? Mr. Dursley wondered. Trying to pull himself together, he let himself into the house. He was still determined not to mention anything to his wife. 49 | 50 | Mrs. Dursley had had a nice, normal day. She told him over dinner all about Mrs. Next Door's problems with her daughter and how Dudley had learned a new word ("Won't!"). Mr. Dursley tried to act normally. When Dudley had been put to bed, he went into the living room in time to catch the last report on the evening news: 51 | 52 | "And finally, bird-watchers everywhere have reported that the nation's owls have been behaving very unusually today. Although owls normally hunt at night and are hardly ever seen in daylight, there have been hundreds of sightings of these birds flying in every direction since sunrise. Experts are unable to explain why the owls have suddenly changed their sleeping pattern." The newscaster allowed himself a grin. "Most mysterious. And now, over to Jim McGuffin with the weather. Going to be any more showers of owls tonight, Jim?" 53 | 54 | "Well, Ted," said the weatherman, "I don't know about that, but it's not only the owls that have been acting oddly today. Viewers as far apart as Kent, Yorkshire, and Dundee have been phoning in to tell me that instead of the rain I promised yesterday, they've had a downpour of shooting stars! Perhaps people have been celebrating Bonfire Night early -- it's not until next week, folks! But I can promise a wet night tonight." 55 | 56 | Mr. Dursley sat frozen in his armchair. Shooting stars all over Britain? Owls flying by daylight? Mysterious people in cloaks all over the place? And a whisper, a whisper about the Potters... 57 | 58 | Mrs. Dursley came into the living room carrying two cups of tea. It was no good. He'd have to say something to her. He cleared his throat nervously. "Er -- Petunia, dear -- you haven't heard from your sister lately, have you?" 59 | 60 | As he had expected, Mrs. Dursley looked shocked and angry. After all, they normally pretended she didn't have a sister. 61 | 62 | "No," she said sharply. "Why?" 63 | 64 | "Funny stuff on the news," Mr. Dursley mumbled. "Owls... shooting 65 | stars... and there were a lot of funny-looking people in town today..." 66 | 67 | "So?" snapped Mrs. Dursley. 68 | 69 | "Well, I just thought... maybe... it was something to do with... you 70 | know... her crowd." 71 | 72 | Mrs. Dursley sipped her tea through pursed lips. Mr. Dursley wondered whether he dared tell her he'd heard the name "Potter." He decided he didn't dare. Instead he said, as casually as he could, "Their son -- he'd be about Dudley's age now, wouldn't he?" 73 | 74 | "I suppose so," said Mrs. Dursley stiffly. 75 | 76 | "What's his name again? Howard, isn't it?" 77 | 78 | "Harry. Nasty, common name, if you ask me." 79 | 80 | "Oh, yes," said Mr. Dursley, his heart sinking horribly. "Yes, I quite 81 | agree." 82 | 83 | He didn't say another word on the subject as they went upstairs to bed. While Mrs. Dursley was in the bathroom, Mr. Dursley crept to the bedroom window and peered down into the front garden. The cat was still there. It was staring down Privet Drive as though it were waiting for something. 84 | 85 | Was he imagining things? Could all this have anything to do with the Potters? If it did... if it got out that they were related to a pair of -- well, he didn't think he could bear it. 86 | 87 | The Dursleys got into bed. Mrs. Dursley fell asleep quickly but Mr. 88 | Dursley lay awake, turning it all over in his mind. His last, comforting thought before he fell asleep was that even if the Potters were involved, there was no reason for them to come near him and Mrs. Dursley. The Potters knew very well what he and Petunia thought about them and their kind.... He couldn’t see how he 89 | and Petunia could get mixed up in anything that might be going on — he yawned and turned over — it couldn’t affect them . . . 90 | How very wrong he was. 91 | Mr. Dursley might have been drifting into an uneasy sleep, but 92 | the cat on the wall outside was showing no sign of sleepiness. It was sitting as still as a statue, its eyes fixed unblinkingly on the far corner of Privet Drive. It didn’t so much as quiver when a car door slammed on the next street, nor when two owls swooped overhead. 93 | In fact, it was nearly midnight before the cat moved at all. 94 | 95 | A man appeared on the corner the cat had been watching, appeared so suddenly and silently you'd have thought he'd just popped out of the ground. The cat's tail twitched and its eyes narrowed. 96 | 97 | Nothing like this man had ever been seen on Privet Drive. He was tall, thin, and very old, judging by the silver of his hair and beard, which were both long enough to tuck into his belt. He was wearing long robes, a purple cloak that swept the ground, and high-heeled, buckled boots. His blue eyes were light, bright, and sparkling behind half-moon spectacles and his nose was very long and crooked, as though it had been broken at least twice. This man's name was Albus Dumbledore. 98 | 99 | Albus Dumbledore didn't seem to realize that he had just arrived in a street where everything from his name to his boots was unwelcome. He was busy rummaging in his cloak, looking for something. But he did seem to realize he was being watched, because he looked up suddenly at the cat, which was still staring at him from the other end of the street. For some reason, the sight of the cat seemed to amuse him. He chuckled and muttered, "I should have known." 100 | 101 | He found what he was looking for in his inside pocket. It seemed to be a silver cigarette lighter. He flicked it open, held it up in the air, and clicked it. The nearest street lamp went out with a little pop. He clicked it again -- the next lamp flickered into darkness. Twelve times he clicked the Put-Outer, until the only lights left on the whole street were two tiny pinpricks in the distance, which were the eyes of the cat watching him. If anyone looked out of their window now, even beady-eyed Mrs. Dursley, they wouldn't be able to see anything that was happening down on the pavement. Dumbledore slipped the Put-Outer back inside his cloak and set off down the street toward number four, where he sat down on the wall next to the cat. He didn't look at it, but after a moment he spoke to it. 102 | 103 | "Fancy seeing you here, Professor McGonagall." 104 | 105 | He turned to smile at the tabby, but it had gone. Instead he was smiling at a rather severe-looking woman who was wearing square glasses exactly the shape of the markings the cat had had around its eyes. She, too, was wearing a cloak, an emerald one. Her black hair was drawn into a tight bun. She looked distinctly ruffled. 106 | 107 | "How did you know it was me?" she asked. 108 | 109 | "My dear Professor, I 've never seen a cat sit so stiffly." 110 | 111 | "You'd be stiff if you'd been sitting on a brick wall all day," said 112 | Professor McGonagall. 113 | 114 | "All day? When you could have been celebrating? I must have passed a dozen feasts and parties on my way here."' 115 | 116 | Professor McGonagall sniffed angrily. 117 | 118 | "Oh yes, everyone's celebrating, all right," she said impatiently. "You'd think they'd be a bit more careful, but no -- even the Muggles have noticed something's going on. It was on their news." She jerked her head back at the Dursleys' dark living-room window. "I heard it. Flocks of owls... shooting stars.... Well, they're not completely stupid. They were bound to notice something. Shooting stars down in Kent -- I'll bet that was Dedalus Diggle. He never had much sense." 119 | 120 | "You can't blame them," said Dumbledore gently. "We've had precious little to celebrate for eleven years." 121 | 122 | "I know that," said Professor McGonagall irritably. "But that's no 123 | reason to lose our heads. People are being downright careless, out on the streets in broad daylight, not even dressed in Muggle clothes, swapping rumors." 124 | 125 | She threw a sharp, sideways glance at Dumbledore here, as though hoping he was going to tell her something, but he didn't, so she went on. "A fine thing it would be if, on the very day You-Know-Who seems to have disappeared at last, the Muggles found out about us all. I suppose he really has gone, Dumbledore?" 126 | 127 | "It certainly seems so," said Dumbledore. "We have much to be thankful for. Would you care for a lemon drop?" 128 | 129 | "A what?" 130 | 131 | "A lemon drop. They're a kind of Muggle sweet I'm rather fond of." 132 | 133 | "No, thank you," said Professor McGonagall coldly, as though she didn't think this was the moment for lemon drops. "As I say, even if 134 | You-Know-Who has gone -" 135 | 136 | "My dear Professor, surely a sensible person like yourself can call him by his name? All this 'You- Know-Who' nonsense -- for eleven years I have been trying to persuade people to call him by his proper name: Voldemort." Professor McGonagall flinched, but Dumbledore, who was unsticking two lemon drops, seemed not to notice. "It all gets so confusing if we keep saying 'You-Know-Who.' I have never seen any reason to be frightened of saying Voldemort's name. 137 | 138 | "I know you haven 't, said Professor McGonagall, sounding half exasperated, half admiring. "But you're different. Everyone knows you're the only one You-Know- oh, all right, Voldemort, was frightened of." 139 | 140 | "You flatter me," said Dumbledore calmly. "Voldemort had powers I will never have." 141 | 142 | "Only because you're too -- well -- noble to use them." 143 | 144 | "It's lucky it's dark. I haven't blushed so much since Madam Pomfrey told me she liked my new earmuffs." 145 | 146 | Professor McGonagall shot a sharp look at Dumbledore and said, "The owls are nothing next to the rumors that are flying around. You know what everyone's saying? About why he's disappeared? About what finally stopped him?" 147 | 148 | It seemed that Professor McGonagall had reached the point she was most anxious to discuss, the real reason she had been waiting on a cold, hard wall all day, for neither as a cat nor as a woman had she fixed Dumbledore with such a piercing stare as she did now. It was plain that whatever "everyone" was saying, she was not going to believe it until Dumbledore told her it was true. Dumbledore, however, was choosing another lemon drop and did not answer. 149 | 150 | "What they're saying," she pressed on, "is that last night Voldemort turned up in Godric's Hollow. He went to find the Potters. The rumor is that Lily and James Potter are -- are -- that they're -- dead." 151 | 152 | Dumbledore bowed his head. Professor McGonagall gasped. 153 | 154 | "Lily and James... I can't believe it... I didn't want to believe it... Oh, Albus..." 155 | 156 | Dumbledore reached out and patted her on the shoulder. "I know... I know..." he said heavily. 157 | 158 | Professor McGonagall's voice trembled as she went on. "That's not all. They're saying he tried to kill the Potter's son, Harry. But -- he couldn't. He couldn't kill that little boy. No one knows why, or how, but they're saying that when he couldn't kill Harry Potter, Voldemort's power somehow broke -- and that's why he's gone. 159 | 160 | Dumbledore nodded glumly. 161 | 162 | "It's -- it's true?" faltered Professor McGonagall. "After all he's done... all the people he's killed... he couldn't kill a little boy? It's just astounding... of all the things to stop him... but how in the name of heaven did Harry survive?" 163 | 164 | "We can only guess," said Dumbledore. "We may never know." 165 | 166 | Professor McGonagall pulled out a lace handkerchief and dabbed at her eyes beneath her spectacles. Dumbledore gave a great sniff as he took a golden watch from his pocket and examined it. It was a very odd watch. It had twelve hands but no numbers; instead, little planets were moving around the edge. It must have made sense to Dumbledore, though, because he put it back in his pocket and said, "Hagrid's late. I suppose it was he who told you I'd be here, by the way?" 167 | 168 | "Yes," said Professor McGonagall. "And I don't suppose you're going to tell me why you're here, of all places?" 169 | 170 | "I've come to bring Harry to his aunt and uncle. They're the only family he has left now." 171 | 172 | "You don't mean -- you can't mean the people who live here?" cried Professor McGonagall, jumping to her feet and pointing at number four. "Dumbledore -- you can't. I've been watching them all day. You couldn't find two people who are less like us. And they've got this son -- I saw him kicking his mother all the way up the street, screaming for sweets. Harry Potter come and live here!" 173 | 174 | "It's the best place for him," said Dumbledore firmly. "His aunt and uncle will be able to explain everything to him when he's older. I've written them a letter." 175 | 176 | "A letter?" repeated Professor McGonagall faintly, sitting back down on the wall. "Really, Dumbledore, you think you can explain all this in a letter? These people will never understand him! He'll be famous -- a legend -- I wouldn't be surprised if today was known as Harry Potter day in the future -- there will be books written about Harry -- every child in our world will know his name!" 177 | 178 | “Exactly,” said Dumbledore, looking very seriously over the top of his half-moon glasses. “It would be enough to turn any boy’s head. Famous before he can walk and talk! Famous for something he won’t even remember! Can’t you see how much better off he’ll be, growing up away from all that until he’s ready to take it?” 179 | Professor McGonagall opened her mouth, changed her mind, 180 | swallowed, and then said, “Yes — yes, you’re right, of course. But how is the boy getting here, Dumbledore?” She eyed his cloak suddenly as though she thought he might be hiding Harry underneath it. 181 | “Hagrid’s bringing him.” 182 | “You think it — wise — to trust Hagrid with something as important as this?” 183 | “I would trust Hagrid with my life,” said Dumbledore. 184 | “I’m not saying his heart isn’t in the right place,” said Professor McGonagall grudgingly, “but you can’t pretend he’s not careless. 185 | He does tend to — what was that?” 186 | A low rumbling sound had broken the silence around them. It 187 | grew steadily louder as they looked up and down the street for some sign of a headlight; it swelled to a roar as they both looked up at the sky — and a huge motorcycle fell out of the air and landed on the road in front of them. 188 | 189 | If the motorcycle was huge, it was nothing to the man sitting astride it. He was almost twice as tall as a normal man and at least five times as wide. He looked simply too big to be allowed, and so wild - long tangles of bushy black hair and beard hid most of his face, he had hands the size of trash can lids, and his feet in their leather boots were like baby dolphins. In his vast, muscular arms he was holding a bundle of blankets. 190 | 191 | "Hagrid," said Dumbledore, sounding relieved. "At last. And where did you get that motorcycle?" 192 | 193 | "Borrowed it, Professor Dumbledore, sir," said the giant, climbing carefully off the motorcycle as he spoke. "Young Sirius Black lent it to me. I've got him, sir." 194 | 195 | "No problems, were there?" 196 | 197 | "No, sir -- house was almost destroyed, but I got him out all right 198 | before the Muggles started swarmin' around. He fell asleep as we was flyin' over Bristol." 199 | 200 | Dumbledore and Professor McGonagall bent forward over the bundle of blankets. Inside, just visible, was a baby boy, fast asleep. Under a tuft of jet-black hair over his forehead they could see a curiously shaped cut, like a bolt of lightning. 201 | 202 | "Is that where -?" whispered Professor McGonagall. 203 | 204 | "Yes," said Dumbledore. "He'll have that scar forever." 205 | 206 | "Couldn't you do something about it, Dumbledore?" 207 | 208 | "Even if I could, I wouldn't. Scars can come in handy. I have one myself above my left knee that is a perfect map of the London Underground. Well -- give him here, Hagrid -- we'd better get this over with." 209 | 210 | Dumbledore took Harry in his arms and turned toward the Dursleys' house. "Could I -- could I say good-bye to him, sir?" asked Hagrid. He bent his great, shaggy head over Harry and gave him what must have been a very scratchy, whiskery kiss. Then, suddenly, Hagrid let out a howl like a wounded dog. 211 | 212 | "Shhh!" hissed Professor McGonagall, "you'll wake the Muggles!" 213 | 214 | "S-s-sorry," sobbed Hagrid, taking out a large, spotted handkerchief and burying his face in it. "But I c-c-can't stand it -- Lily an' James dead -- an' poor little Harry off ter live with Muggles -" 215 | 216 | "Yes, yes, it's all very sad, but get a grip on yourself, Hagrid, or we'll be found," Professor McGonagall whispered, patting Hagrid gingerly on the arm as Dumbledore stepped over the low garden wall and walked to the front door. He laid Harry gently on the doorstep, took a letter out of his cloak, tucked it inside Harry's blankets, and then came back to the other two. For a full minute the three of them stood and looked at the little bundle; Hagrid's shoulders shook, Professor McGonagall blinked furiously, and the twinkling light that usually shone from Dumbledore's eyes seemed to have gone out. 217 | 218 | "Well," said Dumbledore finally, "that's that. We've no business staying here. We may as well go and join the celebrations." 219 | 220 | "Yeah," said Hagrid in a very muffled voice, "I'll be takin' Sirius his bike back. G'night, Professor McGonagall -- Professor Dumbledore, sir." 221 | 222 | Wiping his streaming eyes on his jacket sleeve, Hagrid swung himself onto the motorcycle and kicked the engine into life; with a roar it rose into the air and off into the night. 223 | 224 | "I shall see you soon, I expect, Professor McGonagall," said Dumbledore, nodding to her. Professor McGonagall blew her nose in reply. 225 | 226 | Dumbledore turned and walked back down the street. On the corner he stopped and took out the silver Put-Outer. He clicked it once, and twelve balls of light sped back to their street lamps so that Privet Drive glowed suddenly orange and he could make out a tabby cat slinking around the corner at the other end of the street. He could just see the bundle of blankets on the step of number four. 227 | 228 | "Good luck, Harry," he murmured. He turned on his heel and with a swish of his cloak, he was gone. 229 | 230 | A breeze ruffled the neat hedges of Privet Drive, which lay silent and tidy under the inky sky, the very last place you would expect 231 | astonishing things to happen. Harry Potter rolled over inside his 232 | blankets without waking up. One small hand closed on the letter beside him and he slept on, not knowing he was special, not knowing he was famous, not knowing he would be woken in a few hours' time by Mrs. Dursley's scream as she opened the front door to put out the milk bottles, nor that he would spend the next few weeks being prodded and pinched by his cousin Dudley... He couldn't know that at this very moment, people meeting in secret all over the country were holding up their glasses and saying in hushed voices: "To Harry Potter -- the boy who lived!" 233 | -------------------------------------------------------------------------------- /various/mail.py: -------------------------------------------------------------------------------- 1 | from email.mime.multipart import MIMEMultipart 2 | from email.mime.text import MIMEText 3 | import smtplib 4 | 5 | PYBITES_EMAIL = 'pybitesblog@gmail.com' 6 | 7 | def email(subject, content, recipients=None): 8 | if not recipients: 9 | recipients = [PYBITES_EMAIL] 10 | msg = MIMEMultipart('alternative') 11 | msg['Subject'] = subject 12 | msg['From'] = PYBITES_EMAIL 13 | msg['To'] = ", ".join(recipients) 14 | part = MIMEText(content, 'html') 15 | msg.attach(part) 16 | s = smtplib.SMTP('localhost') 17 | s.sendmail(PYBITES_EMAIL, recipients, msg.as_string()) 18 | s.quit() 19 | 20 | if __name__ == "__main__": 21 | email(["info@bobbelderbos.com"], "test mail", "hello bob") 22 | -------------------------------------------------------------------------------- /various/pybites_party.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | from datetime import date 4 | 5 | from mail import email, PYBITES_EMAIL 6 | 7 | ERROR_MSG = 'expected result for input var {} = {}' 8 | DAYS_IN_YEAR = 365 9 | SPECIAL_DAY_OFFSETS = (100, DAYS_IN_YEAR) 10 | TODAY = date.today() 11 | PYBITES_START = date(year=2016, month=12, day=19) 12 | AGE_DAYS = (TODAY - PYBITES_START).days 13 | PYBITES = 'PyBites' 14 | 15 | def today_is_special_day(age=None): 16 | """ 17 | Returns bool if today is a special day (yearly birthday or n of 100 days 18 | """ 19 | if age is None: 20 | age = AGE_DAYS 21 | return any(map(lambda x: age % x == 0, SPECIAL_DAY_OFFSETS)) 22 | 23 | 24 | def days_till_special_day(age=None): 25 | """ 26 | Calculates days till next special day 27 | """ 28 | if age is None: 29 | age = AGE_DAYS 30 | return min(map(lambda x: x - age % x, SPECIAL_DAY_OFFSETS)) 31 | 32 | 33 | if __name__ == "__main__": 34 | def test_today_is_special_day(): 35 | """ 36 | Tests for range of values if special date 37 | """ 38 | test_days = (1, 50, 100, 101, 200, 305, 365, 400, 499, 730, 800, 850) 39 | test_outcomes = (False, False, True, False, True, False, 40 | True, True, False, True, True, False) 41 | for age, outcome in zip(test_days, test_outcomes): 42 | assert today_is_special_day(age) == outcome, ERROR_MSG.format(age, outcome) 43 | 44 | def test_days_till_special_day(): 45 | """ 46 | Tests for range of values how many days till next special date 47 | """ 48 | test_days = (1, 50, 100, 101, 200, 305, 365, 400, 499, 730, 800, 850) 49 | test_outcomes = (99, 50, 100, 99, 100, 60, 35, 100, 1, 70, 100, 50) 50 | for age, outcome in zip(test_days, test_outcomes): 51 | assert days_till_special_day(age) == outcome, ERROR_MSG.format(age, outcome) 52 | 53 | test_today_is_special_day() 54 | test_days_till_special_day() 55 | 56 | print('{} is {} days old'.format(PYBITES, AGE_DAYS)) 57 | print('Does {} have a birthday today?'.format(PYBITES)) 58 | 59 | if today_is_special_day(): 60 | print('Yes! Sending celebration email') 61 | whatday = 'birthday' if AGE_DAYS % DAYS_IN_YEAR == 0 else 'celebration day' 62 | subject = 'Happy {}!'.format(whatday) 63 | message = '{} exists {} days today, go celebrate!'.format(PYBITES, AGE_DAYS) 64 | email(subject, message) 65 | else: 66 | print('No ... days till next birtday: {}'.format(days_till_special_day())) 67 | -------------------------------------------------------------------------------- /various/subscribers.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | import sys 3 | 4 | try: 5 | subscriber_file = sys.argv[1] 6 | except IndexError: 7 | sys.exit('provide filename') 8 | 9 | try: 10 | with open(subscriber_file) as f: 11 | lines = f.readlines() 12 | except IOError: 13 | sys.exit('file not present') 14 | 15 | stats = Counter(line.rstrip().split(',')[-1] for line in lines[1:]) 16 | 17 | for url, count in stats.most_common(): 18 | print('{:<4} {}'.format(count, url)) 19 | --------------------------------------------------------------------------------