├── .gitignore ├── Procfile ├── README.md ├── app.py ├── requirements.txt ├── runtime.txt ├── static ├── css │ └── style.css └── img │ └── pytip-app.png ├── tasks ├── __init__.py └── import_tweets.py ├── tests ├── __init__.py ├── test_tasks.py ├── test_tips.py └── tweets.json ├── tips ├── __init__.py ├── db.py └── models.py └── views ├── footer.tpl ├── header.tpl └── index.tpl /.gitignore: -------------------------------------------------------------------------------- 1 | *swp 2 | *db 3 | .cache 4 | **__pycache__/ 5 | venv 6 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: python ./app.py 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Daily Python Tips 2 | 3 | [![BCH compliance](https://bettercodehub.com/edge/badge/pybites/pytip?branch=master)](https://bettercodehub.com/) 4 | 5 | App built for [Code Challenge 40 - Daily Python Tip Part 1 - Make a Web App](https://pybit.es/codechallenge40.html) 6 | 7 | I deployed it to [https://pytip.herokuapp.com](https://pytip.herokuapp.com) 8 | 9 | ![python pybites code challenge 40 pytip bottle app](static/img/pytip-app.png) 10 | 11 | ## Setup 12 | 13 | To run this app locally: 14 | 15 | * Create a virtual env 16 | * Create a DB called *pytip* (I use Postgres) 17 | * Add env variables to venv/bin/activate: 18 | 19 | * Twitter API: CONSUMER_KEY / CONSUMER_SECRET / ACCESS_TOKEN / ACCESS_SECRET 20 | * DATABASE_URL = 'postgres://user:pw@localhost:5432/pytip' (tests use pytip_tests) 21 | * If your run it on Heroku set APP_LOCATION to *heroku* (similar deployment instructions as [prchecker](https://github.com/pybites/prchecker)) 22 | 23 | * Install the dependencies: 24 | 25 | pip install -r requirements.txt 26 | 27 | * Get the [python_tip](https://twitter.com/python_tip) tweets: 28 | 29 | (venv) $ python tasks/import_tweets.py 30 | 31 | * To run the tests create a postgres DB called *pytip_test*, then: 32 | 33 | (venv) $ pytest 34 | === test session starts === 35 | platform darwin -- Python 3.6.1, pytest-3.2.3, py-1.4.34, pluggy-0.4.0 36 | rootdir: /Users/bbelderb/Documents/code/pytip, inifile: 37 | collected 5 items 38 | 39 | tests/test_tasks.py . 40 | tests/test_tips.py .... 41 | 42 | === 5 passed in 1.04 seconds === 43 | 44 | * Run the Bottle app: 45 | 46 | (venv) $ python app.py 47 | Bottle v0.12.13 server starting up (using WSGIRefServer())... 48 | Listening on http://localhost:8080/ 49 | Hit Ctrl-C to quit. 50 | 51 | * Enjoy! 52 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from bottle import route, run, request, static_file, view 4 | 5 | from tips import get_hashtags, get_tips 6 | 7 | 8 | @route('/static/') 9 | def send_static(filename): 10 | return static_file(filename, root='static') 11 | 12 | 13 | @route('/') 14 | @route('/') 15 | @view('index') 16 | def index(tag=None): 17 | tag = tag or request.query.get('tag') or None 18 | tags = get_hashtags() 19 | tips = get_tips(tag) 20 | 21 | return {'search_tag': tag or '', 22 | 'tags': tags, 23 | 'tips': tips} 24 | 25 | 26 | if os.environ.get('APP_LOCATION') == 'heroku': 27 | run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000))) 28 | else: 29 | run(host='localhost', port=8080, debug=True, reloader=True) 30 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | attrs==19.3.0 2 | bottle==0.12.20 3 | certifi==2023.7.22 4 | chardet==3.0.4 5 | idna==2.6 6 | more-itertools==8.3.0 7 | oauthlib==3.1.0 8 | packaging==20.4 9 | pluggy==0.13.1 10 | psycopg2==2.8.5 11 | psycopg2-binary==2.8.5 12 | py==1.10.0 13 | pyparsing==2.4.7 14 | PySocks==1.7.1 15 | pytest==5.4.2 16 | requests==2.23.0 17 | requests-oauthlib==1.3.0 18 | six==1.15.0 19 | SQLAlchemy==1.3.17 20 | tweepy==3.8.0 21 | urllib3==1.26.5 22 | wcwidth==0.1.9 23 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6.2 2 | -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Body CSS 3 | */ 4 | html, 5 | body { 6 | height: 100%; 7 | } 8 | 9 | html, 10 | body, 11 | input, 12 | textarea, 13 | buttons { 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.004); 17 | } 18 | 19 | 20 | /** 21 | * Sidebar CSS 22 | */ 23 | #sidebar { 24 | background-color: #fff; 25 | padding: 15px; 26 | } 27 | 28 | @media (min-width: 768px) { 29 | #sidebar { 30 | position: fixed; 31 | top: 0; 32 | bottom: 0; 33 | width: 310px; 34 | height: 100%; 35 | padding-top: 30px; 36 | } 37 | } 38 | 39 | /** 40 | * Content CSS 41 | */ 42 | @media (min-width: 768px) { 43 | #content { 44 | margin-left: 260px; 45 | } 46 | } 47 | 48 | pre { 49 | white-space: pre-wrap; /* css-3 */ 50 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ 51 | white-space: -pre-wrap; /* Opera 4-6 */ 52 | white-space: -o-pre-wrap; /* Opera 7 */ 53 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 54 | } 55 | img.logo { 56 | width: 150px; 57 | } 58 | .tip { 59 | background-color: #f2f2f2; 60 | margin: 20px 0; 61 | padding: 10px; 62 | border-radius: 10px; 63 | } 64 | -------------------------------------------------------------------------------- /static/img/pytip-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybites/pytip/83b02abd0faf40d1db782fcddead33e3bed2339e/static/img/pytip-app.png -------------------------------------------------------------------------------- /tasks/__init__.py: -------------------------------------------------------------------------------- 1 | from tasks.import_tweets import (get_tweets, get_hashtag_counter, 2 | import_tweets, import_hashtags) 3 | -------------------------------------------------------------------------------- /tasks/import_tweets.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from collections import Counter 4 | import os 5 | import re 6 | import sys 7 | 8 | import tweepy 9 | 10 | sys.path.append(os.path.join(os.path.dirname(__file__), '..')) 11 | 12 | from tips import add_tips, truncate_tables, get_tips, add_hashtags 13 | 14 | CONSUMER_KEY = os.environ.get('CONSUMER_KEY') 15 | CONSUMER_SECRET = os.environ.get('CONSUMER_SECRET') 16 | ACCESS_TOKEN = os.environ.get('ACCESS_TOKEN') 17 | ACCESS_SECRET = os.environ.get('ACCESS_SECRET') 18 | 19 | TWITTER_ACCOUNT = os.environ.get('PYTIP_APP_TWITTER_ACCOUNT') or 'python_tip' 20 | EXCLUDE_PYTHON_HASHTAG = True 21 | TAG = re.compile(r'#([a-z0-9]{3,})') 22 | 23 | 24 | def _get_twitter_api_session(): 25 | auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET) 26 | auth.set_access_token(ACCESS_TOKEN, ACCESS_SECRET) 27 | return tweepy.API(auth) 28 | 29 | 30 | def get_tweets(screen_name=TWITTER_ACCOUNT): 31 | api = _get_twitter_api_session() 32 | return tweepy.Cursor(api.user_timeline, 33 | screen_name=screen_name, 34 | exclude_replies=True, 35 | include_rts=False) 36 | 37 | 38 | def get_hashtag_counter(tips): 39 | blob = ' '.join(t.text.lower() for t in tips) 40 | cnt = Counter(TAG.findall(blob)) 41 | 42 | if EXCLUDE_PYTHON_HASHTAG: 43 | cnt.pop('python', None) 44 | 45 | return cnt 46 | 47 | 48 | def import_tweets(tweets=None): 49 | if tweets is None: 50 | tweets = get_tweets(screen_name) 51 | add_tips(tweets) 52 | 53 | 54 | def import_hashtags(): 55 | tips = get_tips() 56 | hashtags_cnt = get_hashtag_counter(tips) 57 | add_hashtags(hashtags_cnt) 58 | 59 | 60 | if __name__ == '__main__': 61 | try: 62 | screen_name = sys.argv[1] 63 | except IndexError: 64 | screen_name = TWITTER_ACCOUNT 65 | 66 | truncate_tables() 67 | 68 | import_tweets() 69 | import_hashtags() 70 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pybites/pytip/83b02abd0faf40d1db782fcddead33e3bed2339e/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_tasks.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple, Counter 2 | 3 | from tasks import get_hashtag_counter 4 | 5 | tip = namedtuple('Tip', 'text') 6 | 7 | 8 | def test_get_hashtag_counter(): 9 | blob = '''a lot of tweets with #jupyter hashtags, some #python, more 10 | #itertools, ah and of course lot of #numpy #NUMpy #NumPY 11 | #pandas is our favorite we got tons of that #pandas #pandas 12 | #pandas ok do some more #pandas and #jupyter and #python3''' 13 | tips = blob.split(',') 14 | tips = [tip(text=t) for t in tips] 15 | expected = Counter({'pandas': 5, 16 | 'numpy': 3, 17 | 'jupyter': 2, 18 | 'itertools': 1, 19 | 'python3': 1}) 20 | assert get_hashtag_counter(tips) == expected 21 | -------------------------------------------------------------------------------- /tests/test_tips.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple, Counter 2 | import json 3 | import os 4 | 5 | import pytest 6 | 7 | from tips import (truncate_tables, get_hashtags, 8 | add_hashtags, get_tips, add_tips) 9 | from tasks import import_tweets, import_hashtags 10 | 11 | tweet = namedtuple('Tweet', 'id text created_at favorite_count retweet_count') 12 | 13 | 14 | def _gen_tweets(): 15 | tweets = os.path.join('tests', 'tweets.json') 16 | 17 | with open(tweets) as f: 18 | data = json.loads(f.read()) 19 | 20 | for entry in data: 21 | yield tweet(id=entry['id'], 22 | text=entry['text'], 23 | created_at=entry['created_at'], 24 | favorite_count=entry['favorite_count'], 25 | retweet_count=entry['retweet_count']) 26 | 27 | 28 | @pytest.fixture() 29 | def db_setup(request): 30 | 31 | tweets = list(_gen_tweets()) 32 | import_tweets(tweets) 33 | import_hashtags() 34 | 35 | def fin(): 36 | truncate_tables() 37 | 38 | request.addfinalizer(fin) 39 | 40 | 41 | def test_get_tips(db_setup): 42 | tips = get_tips() 43 | assert len(tips) == 10 44 | jupyter_tips = get_tips('jupyter') 45 | assert len(jupyter_tips) == 2 46 | numpy_tips = get_tips('numpy') 47 | assert len(numpy_tips) == 2 48 | 49 | 50 | def test_add_tips(db_setup): 51 | tweets = list(_gen_tweets())[:2] 52 | add_tips(tweets) 53 | tips = get_tips() 54 | assert len(tips) == 12 55 | 56 | 57 | def test_get_hashtags(db_setup): 58 | hashtags = get_hashtags() 59 | assert len(hashtags) == 3 60 | assert hashtags[0].name == 'jupyter' 61 | assert hashtags[0].count == 2 62 | assert hashtags[-1].name == 'selfpromo' 63 | assert hashtags[-1].count == 1 64 | 65 | 66 | def test_add_hashtags(db_setup): 67 | new_hashtags = Counter({'pandas': 5, 68 | 'itertools': 1, 69 | 'python3': 1}) 70 | add_hashtags(new_hashtags) == 6 71 | hashtags = get_hashtags() 72 | assert hashtags[1].name == 'jupyter' 73 | assert hashtags[1].count == 2 74 | assert hashtags[-1].name == 'selfpromo' 75 | assert hashtags[-1].count == 1 76 | -------------------------------------------------------------------------------- /tests/tweets.json: -------------------------------------------------------------------------------- 1 | [{"created_at": "Mon Oct 30 09:58:01 +0000 2017", "id": 924938430819090432, "id_str": "924938430819090432", "text": "Use difflib.SequenceMatcher.find_longest_match() to find longest common substring of two strings.\u2026 https://t.co/nbSihHoQOl", "truncated": true, "entities": {"hashtags": [], "symbols": [], "user_mentions": [], "urls": [{"url": "https://t.co/nbSihHoQOl", "expanded_url": "https://twitter.com/i/web/status/924938430819090432", "display_url": "twitter.com/i/web/status/9\u2026", "indices": [99, 122]}]}, "source": "TweetDeck", "in_reply_to_status_id": null, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_user_id_str": null, "in_reply_to_screen_name": null, "user": {"id": 827002120373207040, "id_str": "827002120373207040", "name": "Daily Python Tip", "screen_name": "python_tip", "location": "", "description": "One #python tip per day. Do you have any? Send it to us. Edited by @karlafej, @simecek, @pybites and... you?", "url": "https://t.co/ajWlVvMhfj", "entities": {"url": {"urls": [{"url": "https://t.co/ajWlVvMhfj", "expanded_url": "http://bit.ly/pythontip", "display_url": "bit.ly/pythontip", "indices": [0, 23]}]}, "description": {"urls": []}}, "protected": false, "followers_count": 1686, "friends_count": 146, "listed_count": 46, "created_at": "Thu Feb 02 03:54:05 +0000 2017", "favourites_count": 593, "utc_offset": -25200, "time_zone": "Pacific Time (US & Canada)", "geo_enabled": false, "verified": false, "statuses_count": 229, "lang": "en", "contributors_enabled": false, "is_translator": false, "is_translation_enabled": false, "profile_background_color": "F5F8FA", "profile_background_image_url": null, "profile_background_image_url_https": null, "profile_background_tile": false, "profile_image_url": "http://pbs.twimg.com/profile_images/828169453095510016/X0iDPdDL_normal.jpg", "profile_image_url_https": "https://pbs.twimg.com/profile_images/828169453095510016/X0iDPdDL_normal.jpg", "profile_banner_url": "https://pbs.twimg.com/profile_banners/827002120373207040/1486288529", "profile_link_color": "1DA1F2", "profile_sidebar_border_color": "C0DEED", "profile_sidebar_fill_color": "DDEEF6", "profile_text_color": "333333", "profile_use_background_image": true, "has_extended_profile": false, "default_profile": true, "default_profile_image": false, "following": true, "follow_request_sent": false, "notifications": false, "translator_type": "none"}, "geo": null, "coordinates": null, "place": null, "contributors": null, "is_quote_status": false, "retweet_count": 14, "favorite_count": 40, "favorited": true, "retweeted": true, "possibly_sensitive": false, "lang": "en"}, 2 | {"created_at": "Fri Oct 27 07:00:02 +0000 2017", "id": 923806478422757376, "id_str": "923806478422757376", "text": "'is not' is a binary operator, different from using is and not separately:\n\n> 'something' is not None\nTrue\n> 'something' is (not None)\nFalse", "truncated": false, "entities": {"hashtags": [], "symbols": [], "user_mentions": [], "urls": []}, "source": "TweetDeck", "in_reply_to_status_id": null, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_user_id_str": null, "in_reply_to_screen_name": null, "user": {"id": 827002120373207040, "id_str": "827002120373207040", "name": "Daily Python Tip", "screen_name": "python_tip", "location": "", "description": "One #python tip per day. Do you have any? Send it to us. Edited by @karlafej, @simecek, @pybites and... you?", "url": "https://t.co/ajWlVvMhfj", "entities": {"url": {"urls": [{"url": "https://t.co/ajWlVvMhfj", "expanded_url": "http://bit.ly/pythontip", "display_url": "bit.ly/pythontip", "indices": [0, 23]}]}, "description": {"urls": []}}, "protected": false, "followers_count": 1686, "friends_count": 146, "listed_count": 46, "created_at": "Thu Feb 02 03:54:05 +0000 2017", "favourites_count": 593, "utc_offset": -25200, "time_zone": "Pacific Time (US & Canada)", "geo_enabled": false, "verified": false, "statuses_count": 229, "lang": "en", "contributors_enabled": false, "is_translator": false, "is_translation_enabled": false, "profile_background_color": "F5F8FA", "profile_background_image_url": null, "profile_background_image_url_https": null, "profile_background_tile": false, "profile_image_url": "http://pbs.twimg.com/profile_images/828169453095510016/X0iDPdDL_normal.jpg", "profile_image_url_https": "https://pbs.twimg.com/profile_images/828169453095510016/X0iDPdDL_normal.jpg", "profile_banner_url": "https://pbs.twimg.com/profile_banners/827002120373207040/1486288529", "profile_link_color": "1DA1F2", "profile_sidebar_border_color": "C0DEED", "profile_sidebar_fill_color": "DDEEF6", "profile_text_color": "333333", "profile_use_background_image": true, "has_extended_profile": false, "default_profile": true, "default_profile_image": false, "following": true, "follow_request_sent": false, "notifications": false, "translator_type": "none"}, "geo": null, "coordinates": null, "place": null, "contributors": null, "is_quote_status": false, "retweet_count": 15, "favorite_count": 30, "favorited": true, "retweeted": true, "lang": "en"}, 3 | {"created_at": "Thu Oct 26 07:00:03 +0000 2017", "id": 923444093010808834, "id_str": "923444093010808834", "text": "'where' finds indexes of all Trues in #NumPy array:\n\n> import numpy as np\n> a = [1,2,0,0,4,0]\n> a != 0\n> np.where(a != 0)\n> np.nonzero(a)", "truncated": false, "entities": {"hashtags": [{"text": "NumPy", "indices": [38, 44]}], "symbols": [], "user_mentions": [], "urls": []}, "source": "TweetDeck", "in_reply_to_status_id": null, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_user_id_str": null, "in_reply_to_screen_name": null, "user": {"id": 827002120373207040, "id_str": "827002120373207040", "name": "Daily Python Tip", "screen_name": "python_tip", "location": "", "description": "One #python tip per day. Do you have any? Send it to us. Edited by @karlafej, @simecek, @pybites and... you?", "url": "https://t.co/ajWlVvMhfj", "entities": {"url": {"urls": [{"url": "https://t.co/ajWlVvMhfj", "expanded_url": "http://bit.ly/pythontip", "display_url": "bit.ly/pythontip", "indices": [0, 23]}]}, "description": {"urls": []}}, "protected": false, "followers_count": 1686, "friends_count": 146, "listed_count": 46, "created_at": "Thu Feb 02 03:54:05 +0000 2017", "favourites_count": 593, "utc_offset": -25200, "time_zone": "Pacific Time (US & Canada)", "geo_enabled": false, "verified": false, "statuses_count": 229, "lang": "en", "contributors_enabled": false, "is_translator": false, "is_translation_enabled": false, "profile_background_color": "F5F8FA", "profile_background_image_url": null, "profile_background_image_url_https": null, "profile_background_tile": false, "profile_image_url": "http://pbs.twimg.com/profile_images/828169453095510016/X0iDPdDL_normal.jpg", "profile_image_url_https": "https://pbs.twimg.com/profile_images/828169453095510016/X0iDPdDL_normal.jpg", "profile_banner_url": "https://pbs.twimg.com/profile_banners/827002120373207040/1486288529", "profile_link_color": "1DA1F2", "profile_sidebar_border_color": "C0DEED", "profile_sidebar_fill_color": "DDEEF6", "profile_text_color": "333333", "profile_use_background_image": true, "has_extended_profile": false, "default_profile": true, "default_profile_image": false, "following": true, "follow_request_sent": false, "notifications": false, "translator_type": "none"}, "geo": null, "coordinates": null, "place": null, "contributors": null, "is_quote_status": false, "retweet_count": 6, "favorite_count": 17, "favorited": true, "retweeted": false, "lang": "en"}, 4 | {"created_at": "Wed Oct 25 22:36:44 +0000 2017", "id": 923317432587927552, "id_str": "923317432587927552", "text": "If anybody wants to practice Web App development, @pybites hosts a simple challenge on our data. #selfpromo https://t.co/xi2eJrqYnX", "truncated": false, "entities": {"hashtags": [{"text": "selfpromo", "indices": [97, 107]}], "symbols": [], "user_mentions": [{"screen_name": "pybites", "name": "Pybites", "id": 812103603116613632, "id_str": "812103603116613632", "indices": [50, 58]}], "urls": [{"url": "https://t.co/xi2eJrqYnX", "expanded_url": "https://twitter.com/pybites/status/923087550079815680", "display_url": "twitter.com/pybites/status\u2026", "indices": [108, 131]}]}, "source": "TweetDeck", "in_reply_to_status_id": null, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_user_id_str": null, "in_reply_to_screen_name": null, "user": {"id": 827002120373207040, "id_str": "827002120373207040", "name": "Daily Python Tip", "screen_name": "python_tip", "location": "", "description": "One #python tip per day. Do you have any? Send it to us. Edited by @karlafej, @simecek, @pybites and... you?", "url": "https://t.co/ajWlVvMhfj", "entities": {"url": {"urls": [{"url": "https://t.co/ajWlVvMhfj", "expanded_url": "http://bit.ly/pythontip", "display_url": "bit.ly/pythontip", "indices": [0, 23]}]}, "description": {"urls": []}}, "protected": false, "followers_count": 1686, "friends_count": 146, "listed_count": 46, "created_at": "Thu Feb 02 03:54:05 +0000 2017", "favourites_count": 593, "utc_offset": -25200, "time_zone": "Pacific Time (US & Canada)", "geo_enabled": false, "verified": false, "statuses_count": 229, "lang": "en", "contributors_enabled": false, "is_translator": false, "is_translation_enabled": false, "profile_background_color": "F5F8FA", "profile_background_image_url": null, "profile_background_image_url_https": null, "profile_background_tile": false, "profile_image_url": "http://pbs.twimg.com/profile_images/828169453095510016/X0iDPdDL_normal.jpg", "profile_image_url_https": "https://pbs.twimg.com/profile_images/828169453095510016/X0iDPdDL_normal.jpg", "profile_banner_url": "https://pbs.twimg.com/profile_banners/827002120373207040/1486288529", "profile_link_color": "1DA1F2", "profile_sidebar_border_color": "C0DEED", "profile_sidebar_fill_color": "DDEEF6", "profile_text_color": "333333", "profile_use_background_image": true, "has_extended_profile": false, "default_profile": true, "default_profile_image": false, "following": true, "follow_request_sent": false, "notifications": false, "translator_type": "none"}, "geo": null, "coordinates": null, "place": null, "contributors": null, "is_quote_status": true, "quoted_status_id": 923087550079815680, "quoted_status_id_str": "923087550079815680", "quoted_status": {"created_at": "Wed Oct 25 07:23:16 +0000 2017", "id": 923087550079815680, "id_str": "923087550079815680", "text": "PyBites Code Challenge #40:\nDaily Python Tip Part 1 - Make a Web App\nhttps://t.co/OMa6BSgSxR\n@python_tip \n#django\u2026 https://t.co/ISNkpJpFYS", "truncated": true, "entities": {"hashtags": [{"text": "django", "indices": [106, 113]}], "symbols": [], "user_mentions": [{"screen_name": "python_tip", "name": "Daily Python Tip", "id": 827002120373207040, "id_str": "827002120373207040", "indices": [93, 104]}], "urls": [{"url": "https://t.co/OMa6BSgSxR", "expanded_url": "https://pybit.es/codechallenge40.html", "display_url": "pybit.es/codechallenge4\u2026", "indices": [69, 92]}, {"url": "https://t.co/ISNkpJpFYS", "expanded_url": "https://twitter.com/i/web/status/923087550079815680", "display_url": "twitter.com/i/web/status/9\u2026", "indices": [115, 138]}]}, "source": "Twitter Web Client", "in_reply_to_status_id": null, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_user_id_str": null, "in_reply_to_screen_name": null, "user": {"id": 812103603116613632, "id_str": "812103603116613632", "name": "Pybites", "screen_name": "pybites", "location": "", "description": "Python code challenges, tutorials and news, one bite a day.", "url": "https://t.co/zKMAX9uopl", "entities": {"url": {"urls": [{"url": "https://t.co/zKMAX9uopl", "expanded_url": "http://pybit.es", "display_url": "pybit.es", "indices": [0, 23]}]}, "description": {"urls": []}}, "protected": false, "followers_count": 1453, "friends_count": 198, "listed_count": 62, "created_at": "Fri Dec 23 01:12:41 +0000 2016", "favourites_count": 1965, "utc_offset": -25200, "time_zone": "Pacific Time (US & Canada)", "geo_enabled": true, "verified": false, "statuses_count": 1877, "lang": "en", "contributors_enabled": false, "is_translator": false, "is_translation_enabled": false, "profile_background_color": "000000", "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", "profile_background_tile": false, "profile_image_url": "http://pbs.twimg.com/profile_images/835296204271587329/7gcs-jNs_normal.jpg", "profile_image_url_https": "https://pbs.twimg.com/profile_images/835296204271587329/7gcs-jNs_normal.jpg", "profile_banner_url": "https://pbs.twimg.com/profile_banners/812103603116613632/1490252112", "profile_link_color": "1B95E0", "profile_sidebar_border_color": "000000", "profile_sidebar_fill_color": "000000", "profile_text_color": "000000", "profile_use_background_image": false, "has_extended_profile": false, "default_profile": false, "default_profile_image": false, "following": false, "follow_request_sent": false, "notifications": false, "translator_type": "none"}, "geo": null, "coordinates": null, "place": {"id": "ecdce75d48b13b64", "url": "https://api.twitter.com/1.1/geo/id/ecdce75d48b13b64.json", "place_type": "country", "name": "Spain", "full_name": "Spain", "country_code": "ES", "country": "Spain", "contained_within": [], "bounding_box": {"type": "Polygon", "coordinates": [[[-18.1606948, 27.6377504], [4.3279851, 27.6377504], [4.3279851, 43.7903881], [-18.1606948, 43.7903881]]]}, "attributes": {}}, "contributors": null, "is_quote_status": false, "retweet_count": 16, "favorite_count": 36, "favorited": false, "retweeted": false, "possibly_sensitive": false, "lang": "en"}, "retweet_count": 10, "favorite_count": 20, "favorited": true, "retweeted": true, "possibly_sensitive": false, "lang": "en"}, 5 | {"created_at": "Wed Oct 25 07:00:03 +0000 2017", "id": 923081705233231872, "id_str": "923081705233231872", "text": "Colab, Google's fork of #Jupyter notebook with remote kernels, is now publicly available at\n\nhttps://t.co/vaeZBJ7var\nhttps://t.co/ewZ8RTeR4K", "truncated": false, "entities": {"hashtags": [{"text": "Jupyter", "indices": [24, 32]}], "symbols": [], "user_mentions": [], "urls": [{"url": "https://t.co/vaeZBJ7var", "expanded_url": "https://colab.research.google.com", "display_url": "colab.research.google.com", "indices": [93, 116]}, {"url": "https://t.co/ewZ8RTeR4K", "expanded_url": "https://research.google.com/colaboratory/faq.html", "display_url": "research.google.com/colaboratory/f\u2026", "indices": [117, 140]}]}, "source": "TweetDeck", "in_reply_to_status_id": null, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_user_id_str": null, "in_reply_to_screen_name": null, "user": {"id": 827002120373207040, "id_str": "827002120373207040", "name": "Daily Python Tip", "screen_name": "python_tip", "location": "", "description": "One #python tip per day. Do you have any? Send it to us. Edited by @karlafej, @simecek, @pybites and... you?", "url": "https://t.co/ajWlVvMhfj", "entities": {"url": {"urls": [{"url": "https://t.co/ajWlVvMhfj", "expanded_url": "http://bit.ly/pythontip", "display_url": "bit.ly/pythontip", "indices": [0, 23]}]}, "description": {"urls": []}}, "protected": false, "followers_count": 1686, "friends_count": 146, "listed_count": 46, "created_at": "Thu Feb 02 03:54:05 +0000 2017", "favourites_count": 593, "utc_offset": -25200, "time_zone": "Pacific Time (US & Canada)", "geo_enabled": false, "verified": false, "statuses_count": 229, "lang": "en", "contributors_enabled": false, "is_translator": false, "is_translation_enabled": false, "profile_background_color": "F5F8FA", "profile_background_image_url": null, "profile_background_image_url_https": null, "profile_background_tile": false, "profile_image_url": "http://pbs.twimg.com/profile_images/828169453095510016/X0iDPdDL_normal.jpg", "profile_image_url_https": "https://pbs.twimg.com/profile_images/828169453095510016/X0iDPdDL_normal.jpg", "profile_banner_url": "https://pbs.twimg.com/profile_banners/827002120373207040/1486288529", "profile_link_color": "1DA1F2", "profile_sidebar_border_color": "C0DEED", "profile_sidebar_fill_color": "DDEEF6", "profile_text_color": "333333", "profile_use_background_image": true, "has_extended_profile": false, "default_profile": true, "default_profile_image": false, "following": true, "follow_request_sent": false, "notifications": false, "translator_type": "none"}, "geo": null, "coordinates": null, "place": null, "contributors": null, "is_quote_status": false, "retweet_count": 6, "favorite_count": 23, "favorited": false, "retweeted": false, "possibly_sensitive": false, "lang": "en"}, 6 | {"created_at": "Tue Oct 24 07:00:01 +0000 2017", "id": 922719309301870593, "id_str": "922719309301870593", "text": "Get a random number from a range with 'randrange':\n\n>>> import random\n>>> random.randrange(0,9,2) + 1\n\n#python", "truncated": false, "entities": {"hashtags": [{"text": "python", "indices": [121, 128]}], "symbols": [], "user_mentions": [], "urls": []}, "source": "TweetDeck", "in_reply_to_status_id": null, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_user_id_str": null, "in_reply_to_screen_name": null, "user": {"id": 827002120373207040, "id_str": "827002120373207040", "name": "Daily Python Tip", "screen_name": "python_tip", "location": "", "description": "One #python tip per day. Do you have any? Send it to us. Edited by @karlafej, @simecek, @pybites and... you?", "url": "https://t.co/ajWlVvMhfj", "entities": {"url": {"urls": [{"url": "https://t.co/ajWlVvMhfj", "expanded_url": "http://bit.ly/pythontip", "display_url": "bit.ly/pythontip", "indices": [0, 23]}]}, "description": {"urls": []}}, "protected": false, "followers_count": 1686, "friends_count": 146, "listed_count": 46, "created_at": "Thu Feb 02 03:54:05 +0000 2017", "favourites_count": 593, "utc_offset": -25200, "time_zone": "Pacific Time (US & Canada)", "geo_enabled": false, "verified": false, "statuses_count": 229, "lang": "en", "contributors_enabled": false, "is_translator": false, "is_translation_enabled": false, "profile_background_color": "F5F8FA", "profile_background_image_url": null, "profile_background_image_url_https": null, "profile_background_tile": false, "profile_image_url": "http://pbs.twimg.com/profile_images/828169453095510016/X0iDPdDL_normal.jpg", "profile_image_url_https": "https://pbs.twimg.com/profile_images/828169453095510016/X0iDPdDL_normal.jpg", "profile_banner_url": "https://pbs.twimg.com/profile_banners/827002120373207040/1486288529", "profile_link_color": "1DA1F2", "profile_sidebar_border_color": "C0DEED", "profile_sidebar_fill_color": "DDEEF6", "profile_text_color": "333333", "profile_use_background_image": true, "has_extended_profile": false, "default_profile": true, "default_profile_image": false, "following": true, "follow_request_sent": false, "notifications": false, "translator_type": "none"}, "geo": null, "coordinates": null, "place": null, "contributors": null, "is_quote_status": false, "retweet_count": 9, "favorite_count": 16, "favorited": true, "retweeted": true, "lang": "en"}, 7 | {"created_at": "Mon Oct 23 19:58:01 +0000 2017", "id": 922552711949799425, "id_str": "922552711949799425", "text": "Use Ctrl (or Cmd on Mac) + mouse clicks for multicursors in #jupyter notebooks.\nhttps://t.co/YetSCWX86D", "truncated": false, "entities": {"hashtags": [{"text": "jupyter", "indices": [60, 68]}], "symbols": [], "user_mentions": [], "urls": [{"url": "https://t.co/YetSCWX86D", "expanded_url": "http://swanintelligence.com/multi-cursor-in-jupyter.html", "display_url": "swanintelligence.com/multi-cursor-i\u2026", "indices": [80, 103]}]}, "source": "TweetDeck", "in_reply_to_status_id": null, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_user_id_str": null, "in_reply_to_screen_name": null, "user": {"id": 827002120373207040, "id_str": "827002120373207040", "name": "Daily Python Tip", "screen_name": "python_tip", "location": "", "description": "One #python tip per day. Do you have any? Send it to us. Edited by @karlafej, @simecek, @pybites and... you?", "url": "https://t.co/ajWlVvMhfj", "entities": {"url": {"urls": [{"url": "https://t.co/ajWlVvMhfj", "expanded_url": "http://bit.ly/pythontip", "display_url": "bit.ly/pythontip", "indices": [0, 23]}]}, "description": {"urls": []}}, "protected": false, "followers_count": 1686, "friends_count": 146, "listed_count": 46, "created_at": "Thu Feb 02 03:54:05 +0000 2017", "favourites_count": 593, "utc_offset": -25200, "time_zone": "Pacific Time (US & Canada)", "geo_enabled": false, "verified": false, "statuses_count": 229, "lang": "en", "contributors_enabled": false, "is_translator": false, "is_translation_enabled": false, "profile_background_color": "F5F8FA", "profile_background_image_url": null, "profile_background_image_url_https": null, "profile_background_tile": false, "profile_image_url": "http://pbs.twimg.com/profile_images/828169453095510016/X0iDPdDL_normal.jpg", "profile_image_url_https": "https://pbs.twimg.com/profile_images/828169453095510016/X0iDPdDL_normal.jpg", "profile_banner_url": "https://pbs.twimg.com/profile_banners/827002120373207040/1486288529", "profile_link_color": "1DA1F2", "profile_sidebar_border_color": "C0DEED", "profile_sidebar_fill_color": "DDEEF6", "profile_text_color": "333333", "profile_use_background_image": true, "has_extended_profile": false, "default_profile": true, "default_profile_image": false, "following": true, "follow_request_sent": false, "notifications": false, "translator_type": "none"}, "geo": null, "coordinates": null, "place": null, "contributors": null, "is_quote_status": false, "retweet_count": 1, "favorite_count": 5, "favorited": false, "retweeted": false, "possibly_sensitive": false, "lang": "en"}, 8 | {"created_at": "Fri Oct 20 08:44:21 +0000 2017", "id": 921296013092163584, "id_str": "921296013092163584", "text": "Memory size of any #numpy array:\n\n>>> import numpy as np\n>>> Z = np.zeros((10,10))\n>>> print(\"%d bytes\" % (Z.size * Z.itemsize))\n\n#python", "truncated": false, "entities": {"hashtags": [{"text": "numpy", "indices": [19, 25]}, {"text": "python", "indices": [157, 164]}], "symbols": [], "user_mentions": [], "urls": []}, "source": "TweetDeck", "in_reply_to_status_id": null, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_user_id_str": null, "in_reply_to_screen_name": null, "user": {"id": 827002120373207040, "id_str": "827002120373207040", "name": "Daily Python Tip", "screen_name": "python_tip", "location": "", "description": "One #python tip per day. Do you have any? Send it to us. Edited by @karlafej, @simecek, @pybites and... you?", "url": "https://t.co/ajWlVvMhfj", "entities": {"url": {"urls": [{"url": "https://t.co/ajWlVvMhfj", "expanded_url": "http://bit.ly/pythontip", "display_url": "bit.ly/pythontip", "indices": [0, 23]}]}, "description": {"urls": []}}, "protected": false, "followers_count": 1686, "friends_count": 146, "listed_count": 46, "created_at": "Thu Feb 02 03:54:05 +0000 2017", "favourites_count": 593, "utc_offset": -25200, "time_zone": "Pacific Time (US & Canada)", "geo_enabled": false, "verified": false, "statuses_count": 229, "lang": "en", "contributors_enabled": false, "is_translator": false, "is_translation_enabled": false, "profile_background_color": "F5F8FA", "profile_background_image_url": null, "profile_background_image_url_https": null, "profile_background_tile": false, "profile_image_url": "http://pbs.twimg.com/profile_images/828169453095510016/X0iDPdDL_normal.jpg", "profile_image_url_https": "https://pbs.twimg.com/profile_images/828169453095510016/X0iDPdDL_normal.jpg", "profile_banner_url": "https://pbs.twimg.com/profile_banners/827002120373207040/1486288529", "profile_link_color": "1DA1F2", "profile_sidebar_border_color": "C0DEED", "profile_sidebar_fill_color": "DDEEF6", "profile_text_color": "333333", "profile_use_background_image": true, "has_extended_profile": false, "default_profile": true, "default_profile_image": false, "following": true, "follow_request_sent": false, "notifications": false, "translator_type": "none"}, "geo": null, "coordinates": null, "place": null, "contributors": null, "is_quote_status": false, "retweet_count": 4, "favorite_count": 22, "favorited": true, "retweeted": true, "lang": "en"}, 9 | {"created_at": "Thu Oct 19 09:19:55 +0000 2017", "id": 920942575858733057, "id_str": "920942575858733057", "text": "Try this:\n>>> a = 256\n>>> b = 256\n>>> a is b\n\n>>> a = 257\n>>> b = 257\n>>> a is b\n\n#python \nhttps://t.co/GygqCOpDwn\nhttps://t.co/EFM5AdZh7N", "truncated": false, "entities": {"hashtags": [{"text": "python", "indices": [136, 143]}], "symbols": [], "user_mentions": [], "urls": [{"url": "https://t.co/GygqCOpDwn", "expanded_url": "https://docs.python.org/3/c-api/long.html", "display_url": "docs.python.org/3/c-api/long.h\u2026", "indices": [145, 168]}, {"url": "https://t.co/EFM5AdZh7N", "expanded_url": "https://stackoverflow.com/questions/306313/is-operator-behaves-unexpectedly-with-integers", "display_url": "stackoverflow.com/questions/3063\u2026", "indices": [169, 192]}]}, "source": "TweetDeck", "in_reply_to_status_id": null, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_user_id_str": null, "in_reply_to_screen_name": null, "user": {"id": 827002120373207040, "id_str": "827002120373207040", "name": "Daily Python Tip", "screen_name": "python_tip", "location": "", "description": "One #python tip per day. Do you have any? Send it to us. Edited by @karlafej, @simecek, @pybites and... you?", "url": "https://t.co/ajWlVvMhfj", "entities": {"url": {"urls": [{"url": "https://t.co/ajWlVvMhfj", "expanded_url": "http://bit.ly/pythontip", "display_url": "bit.ly/pythontip", "indices": [0, 23]}]}, "description": {"urls": []}}, "protected": false, "followers_count": 1686, "friends_count": 146, "listed_count": 46, "created_at": "Thu Feb 02 03:54:05 +0000 2017", "favourites_count": 593, "utc_offset": -25200, "time_zone": "Pacific Time (US & Canada)", "geo_enabled": false, "verified": false, "statuses_count": 229, "lang": "en", "contributors_enabled": false, "is_translator": false, "is_translation_enabled": false, "profile_background_color": "F5F8FA", "profile_background_image_url": null, "profile_background_image_url_https": null, "profile_background_tile": false, "profile_image_url": "http://pbs.twimg.com/profile_images/828169453095510016/X0iDPdDL_normal.jpg", "profile_image_url_https": "https://pbs.twimg.com/profile_images/828169453095510016/X0iDPdDL_normal.jpg", "profile_banner_url": "https://pbs.twimg.com/profile_banners/827002120373207040/1486288529", "profile_link_color": "1DA1F2", "profile_sidebar_border_color": "C0DEED", "profile_sidebar_fill_color": "DDEEF6", "profile_text_color": "333333", "profile_use_background_image": true, "has_extended_profile": false, "default_profile": true, "default_profile_image": false, "following": true, "follow_request_sent": false, "notifications": false, "translator_type": "none"}, "geo": null, "coordinates": null, "place": null, "contributors": null, "is_quote_status": false, "retweet_count": 13, "favorite_count": 20, "favorited": false, "retweeted": false, "possibly_sensitive": false, "lang": "en"}, 10 | {"created_at": "Wed Oct 18 09:06:00 +0000 2017", "id": 920576687364075520, "id_str": "920576687364075520", "text": "Want to stress the variable is throw away? Use _\n\nname, _ = 'https://t.co/wE9MoewaGu'.split('.', 1)\n\nor \n\n[list([0]*10) for _ in range(10)]", "truncated": false, "entities": {"hashtags": [], "symbols": [], "user_mentions": [], "urls": [{"url": "https://t.co/wE9MoewaGu", "expanded_url": "http://bar.bam.foo", "display_url": "bar.bam.foo", "indices": [61, 84]}]}, "source": "TweetDeck", "in_reply_to_status_id": null, "in_reply_to_status_id_str": null, "in_reply_to_user_id": null, "in_reply_to_user_id_str": null, "in_reply_to_screen_name": null, "user": {"id": 827002120373207040, "id_str": "827002120373207040", "name": "Daily Python Tip", "screen_name": "python_tip", "location": "", "description": "One #python tip per day. Do you have any? Send it to us. Edited by @karlafej, @simecek, @pybites and... you?", "url": "https://t.co/ajWlVvMhfj", "entities": {"url": {"urls": [{"url": "https://t.co/ajWlVvMhfj", "expanded_url": "http://bit.ly/pythontip", "display_url": "bit.ly/pythontip", "indices": [0, 23]}]}, "description": {"urls": []}}, "protected": false, "followers_count": 1686, "friends_count": 146, "listed_count": 46, "created_at": "Thu Feb 02 03:54:05 +0000 2017", "favourites_count": 593, "utc_offset": -25200, "time_zone": "Pacific Time (US & Canada)", "geo_enabled": false, "verified": false, "statuses_count": 229, "lang": "en", "contributors_enabled": false, "is_translator": false, "is_translation_enabled": false, "profile_background_color": "F5F8FA", "profile_background_image_url": null, "profile_background_image_url_https": null, "profile_background_tile": false, "profile_image_url": "http://pbs.twimg.com/profile_images/828169453095510016/X0iDPdDL_normal.jpg", "profile_image_url_https": "https://pbs.twimg.com/profile_images/828169453095510016/X0iDPdDL_normal.jpg", "profile_banner_url": "https://pbs.twimg.com/profile_banners/827002120373207040/1486288529", "profile_link_color": "1DA1F2", "profile_sidebar_border_color": "C0DEED", "profile_sidebar_fill_color": "DDEEF6", "profile_text_color": "333333", "profile_use_background_image": true, "has_extended_profile": false, "default_profile": true, "default_profile_image": false, "following": true, "follow_request_sent": false, "notifications": false, "translator_type": "none"}, "geo": null, "coordinates": null, "place": null, "contributors": null, "is_quote_status": false, "retweet_count": 8, "favorite_count": 12, "favorited": true, "retweeted": true, "possibly_sensitive": false, "lang": "en"}] 11 | -------------------------------------------------------------------------------- /tips/__init__.py: -------------------------------------------------------------------------------- 1 | from tips.db import (truncate_tables, get_hashtags, 2 | add_hashtags, get_tips, add_tips) 3 | -------------------------------------------------------------------------------- /tips/db.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | import re 3 | import sys 4 | 5 | from sqlalchemy import create_engine 6 | from sqlalchemy.orm import sessionmaker 7 | 8 | from .models import Base, Hashtag, Tip 9 | 10 | VALID_TAG = re.compile(r'^[a-z0-9]+$') 11 | 12 | 13 | def _create_session(): 14 | db_url = environ.get('DATABASE_URL') 15 | 16 | if 'pytest' in sys.argv[0]: 17 | db_url += '_test' 18 | 19 | if not db_url: 20 | raise EnvironmentError('Need to set (TEST_)DATABASE_URL') 21 | 22 | engine = create_engine(db_url, echo=True) 23 | Base.metadata.create_all(engine) 24 | create_session = sessionmaker(bind=engine) 25 | return create_session() 26 | 27 | 28 | session = _create_session() 29 | 30 | 31 | def truncate_tables(): 32 | session.query(Tip).delete() 33 | session.query(Hashtag).delete() 34 | session.commit() 35 | 36 | 37 | def get_hashtags(): 38 | return session.query(Hashtag).order_by(Hashtag.name.asc()).all() 39 | 40 | 41 | def add_hashtags(hashtags_cnt): 42 | for tag, count in hashtags_cnt.items(): 43 | session.add(Hashtag(name=tag, count=count)) 44 | session.commit() 45 | 46 | 47 | def get_tips(tag=None): 48 | if tag is not None and VALID_TAG.match(tag.lower()): 49 | filter_ = "%{}%".format(tag.lower()) 50 | tips = session.query(Tip) 51 | tips = tips.filter(Tip.text.ilike(filter_)) 52 | else: 53 | tips = session.query(Tip) 54 | 55 | tips = tips.order_by(Tip.likes.desc()) 56 | return tips.all() 57 | 58 | 59 | def add_tips(tweets): 60 | tweets = tweets if isinstance(tweets, list) else tweets.items() 61 | for tw in tweets: 62 | session.add(Tip(tweetid=tw.id, 63 | text=tw.text, 64 | created=tw.created_at, 65 | likes=tw.favorite_count, 66 | retweets=tw.retweet_count)) 67 | session.commit() 68 | -------------------------------------------------------------------------------- /tips/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Sequence, Integer, String, DateTime 2 | from sqlalchemy.ext.declarative import declarative_base 3 | 4 | Base = declarative_base() 5 | 6 | 7 | class Hashtag(Base): 8 | __tablename__ = 'hashtags' 9 | id = Column(Integer, Sequence('id_seq'), primary_key=True) 10 | name = Column(String(20)) 11 | count = Column(Integer) 12 | 13 | def __repr__(self): 14 | return "" % (self.name, self.count) 15 | 16 | 17 | class Tip(Base): 18 | __tablename__ = 'tips' 19 | id = Column(Integer, Sequence('id_seq'), primary_key=True) 20 | tweetid = Column(String(22)) 21 | text = Column(String(300)) 22 | created = Column(DateTime) 23 | likes = Column(Integer) 24 | retweets = Column(Integer) 25 | 26 | def __repr__(self): 27 | return "" % (self.id, self.text) 28 | -------------------------------------------------------------------------------- /views/footer.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /views/header.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Daily Python Tip 11 | 12 | 13 | 14 | 52 | -------------------------------------------------------------------------------- /views/index.tpl: -------------------------------------------------------------------------------- 1 | % include('header.tpl', tags=tags) 2 | 3 |
4 | 5 |
6 |
7 | 8 |
9 |

10 | DAILY PYTHON TIPS ({{ len(tips) }}) 11 | % if search_tag: 12 |  (show all) 13 | % end 14 |

15 |
16 |
17 | % for tip in tips: 18 |
19 |
{{ !tip.text }}
20 |
{{ tip.likes }} Likes / {{ tip.retweets }} RTs / {{ tip.created }} / Share
21 |
22 | % end 23 | 24 |
25 |
26 | 27 |
28 | 29 | % include('footer.tpl') 30 | --------------------------------------------------------------------------------