Letzten Oktober haben wir unser PyBites-Publikum aufgefordert, eine Web-App zu erstellen, um besser im täglichen Python-Tipp-Feed zu navigieren. In diesem Artikel teile ich, was ich dabei aufgebaut und gelernt habe.
In diesem Artikel erfahren Sie:
- So klonen Sie das Projekt-Repository und richten die App ein.
- So verwenden Sie die Twitter-API über das Tweepy-Modul zum Laden der Tweets.
- Verwendung von SQLAlchemy zum Speichern und Verwalten der Daten (Tipps und Hashtags).
- Wie man eine einfache Web-App mit Bottle erstellt, einem Micro-Web-Framework ähnlich wie Flask.
- Verwenden des Pytest-Frameworks zum Hinzufügen von Tests.
- Wie die Anleitung von Better Code Hub zu besser wartbarem Code führte.
Wenn Sie mitmachen und den Code im Detail lesen (und möglicherweise beitragen) möchten, schlage ich vor, dass Sie das Repo forken. Fangen wir an.
Projekteinrichtung
Erstens sind Namespaces eine tolle Idee Lassen Sie uns also unsere Arbeit in einer virtuellen Umgebung erledigen. Mit Anaconda erstelle ich es so:
$ virtualenv -p <path-to-python-to-use> ~/virtualenvs/pytip
Erstellen Sie eine Produktions- und eine Testdatenbank in Postgres:
$ psql
psql (9.6.5, server 9.6.2)
Type "help" for help.
# create database pytip;
CREATE DATABASE
# create database pytip_test;
CREATE DATABASE
Wir benötigen Anmeldeinformationen, um eine Verbindung zur Datenbank und zur Twitter-API herzustellen (erstellen Sie zuerst eine neue App). Gemäß Best Practice sollte die Konfiguration in der Umgebung gespeichert werden, nicht im Code. Fügen Sie die folgenden Umgebungsvariablen am Ende von ~/virtualenvs/pytip/bin/activate ein , das Skript, das die Aktivierung/Deaktivierung Ihrer virtuellen Umgebung handhabt, und stellt sicher, dass die Variablen für Ihre Umgebung aktualisiert werden:
export DATABASE_URL='postgres://postgres:password@localhost:5432/pytip'
# twitter
export CONSUMER_KEY='xyz'
export CONSUMER_SECRET='xyz'
export ACCESS_TOKEN='xyz'
export ACCESS_SECRET='xyz'
# if deploying it set this to 'heroku'
export APP_LOCATION=local
In der Deaktivierungsfunktion desselben Skripts lösche ich sie, damit wir Dinge aus dem Shell-Bereich heraushalten, wenn wir die virtuelle Umgebung deaktivieren (verlassen):
unset DATABASE_URL
unset CONSUMER_KEY
unset CONSUMER_SECRET
unset ACCESS_TOKEN
unset ACCESS_SECRET
unset APP_LOCATION
Jetzt ist ein guter Zeitpunkt, um die virtuelle Umgebung zu aktivieren:
$ source ~/virtualenvs/pytip/bin/activate
Klonen Sie das Repository und installieren Sie bei aktivierter virtueller Umgebung die Anforderungen:
$ git clone https://github.com/pybites/pytip && cd pytip
$ pip install -r requirements.txt
Als nächstes importieren wir die Sammlung von Tweets mit:
$ python tasks/import_tweets.py
Überprüfen Sie dann, ob die Tabellen erstellt und die Tweets hinzugefügt wurden:
$ psql
\c pytip
pytip=# \dt
List of relations
Schema | Name | Type | Owner
--------+----------+-------+----------
public | hashtags | table | postgres
public | tips | table | postgres
(2 rows)
pytip=# select count(*) from tips;
count
-------
222
(1 row)
pytip=# select count(*) from hashtags;
count
-------
27
(1 row)
pytip=# \q
Lassen Sie uns nun die Tests ausführen:
$ pytest
========================== test session starts ==========================
platform darwin -- Python 3.6.2, pytest-3.2.3, py-1.4.34, pluggy-0.4.0
rootdir: realpython/pytip, inifile:
collected 5 items
tests/test_tasks.py .
tests/test_tips.py ....
========================== 5 passed in 0.61 seconds ==========================
Und zuletzt führen Sie die Flaschen-App mit:
aus$ python app.py
Navigieren Sie zu http://localhost:8080 und voilà:Sie sollten die Tipps nach Beliebtheit sortiert sehen. Wenn Sie links auf einen Hashtag-Link klicken oder das Suchfeld verwenden, können Sie sie einfach filtern. Hier sehen wir die Pandas Tipps zum Beispiel:
Das Design habe ich mit MUI erstellt – einem leichtgewichtigen CSS-Framework, das den Material Design-Richtlinien von Google folgt.
Implementierungsdetails
Die Datenbank und SQLAlchemy
Ich habe SQLAlchemy als Schnittstelle zur DB verwendet, um zu verhindern, dass viel (redundantes) SQL geschrieben werden muss.
In tips/models.py , definieren wir unsere Modelle - Hashtag
und Tip
- dass SQLAlchemy DB-Tabellen zuordnet:
from sqlalchemy import Column, Sequence, Integer, String, DateTime
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Hashtag(Base):
__tablename__ = 'hashtags'
id = Column(Integer, Sequence('id_seq'), primary_key=True)
name = Column(String(20))
count = Column(Integer)
def __repr__(self):
return "<Hashtag('%s', '%d')>" % (self.name, self.count)
class Tip(Base):
__tablename__ = 'tips'
id = Column(Integer, Sequence('id_seq'), primary_key=True)
tweetid = Column(String(22))
text = Column(String(300))
created = Column(DateTime)
likes = Column(Integer)
retweets = Column(Integer)
def __repr__(self):
return "<Tip('%d', '%s')>" % (self.id, self.text)
In tips/db.py , importieren wir diese Modelle, und jetzt ist es einfach, mit der DB zu arbeiten, beispielsweise um eine Schnittstelle mit dem Hashtag
herzustellen Modell:
def get_hashtags():
return session.query(Hashtag).order_by(Hashtag.name.asc()).all()
Und:
def add_hashtags(hashtags_cnt):
for tag, count in hashtags_cnt.items():
session.add(Hashtag(name=tag, count=count))
session.commit()
Twitter-API abfragen
Wir müssen die Daten von Twitter abrufen. Dafür habe ich tasks/import_tweets.py erstellt . Ich habe das unter Aufgaben gepackt weil es in einem täglichen Cronjob ausgeführt werden sollte, um nach neuen Tipps zu suchen und Statistiken (Anzahl der Likes und Retweets) zu bestehenden Tweets zu aktualisieren. Der Einfachheit halber lasse ich die Tabellen täglich neu erstellen. Wenn wir anfangen, uns auf FK-Beziehungen mit anderen Tabellen zu verlassen, sollten wir auf jeden Fall update-Anweisungen statt delete+add wählen.
Wir haben dieses Skript im Projekt-Setup verwendet. Sehen wir uns genauer an, was es tut.
Zuerst erstellen wir ein API-Sitzungsobjekt, das wir an tweepy.Cursor übergeben. Dieses Feature der API ist wirklich nett:Es befasst sich mit der Paginierung und dem Iterieren durch die Zeitleiste. Für die Menge an Trinkgeldern – 222 zu der Zeit, als ich dies schreibe – ist es wirklich schnell. Der exclude_replies=True
und include_rts=False
Argumente sind bequem, weil wir nur die eigenen Tweets von Daily Python Tip wollen (keine Retweets).
Das Extrahieren von Hashtags aus den Tipps erfordert sehr wenig Code.
Zuerst habe ich eine Regex für ein Tag definiert:
TAG = re.compile(r'#([a-z0-9]{3,})')
Dann habe ich findall
verwendet um alle Tags zu erhalten.
Ich habe sie an collections.Counter übergeben, das ein dict-ähnliches Objekt mit den Tags als Schlüssel zurückgibt und als Werte zählt, sortiert in absteigender Reihenfolge nach Werten (am häufigsten). Ich habe das zu häufige Python-Tag ausgeschlossen, das die Ergebnisse verfälschen würde.
def get_hashtag_counter(tips):
blob = ' '.join(t.text.lower() for t in tips)
cnt = Counter(TAG.findall(blob))
if EXCLUDE_PYTHON_HASHTAG:
cnt.pop('python', None)
return cnt
Schließlich der import_*
Funktionen in tasks/import_tweets.py Führen Sie den eigentlichen Import der Tweets und Hashtags durch, indem Sie add_*
aufrufen DB-Methoden der Tipps Verzeichnis/Paket.
Erstelle eine einfache Web-App mit Bottle
Mit dieser Vorarbeit ist das Erstellen einer Web-App überraschend einfach (oder nicht so überraschend, wenn Sie zuvor Flask verwendet haben).
Lernen Sie zunächst Flasche kennen:
Bottle ist ein schnelles, einfaches und leichtes WSGI-Mikro-Web-Framework für Python. Es wird als einzelnes Dateimodul verteilt und hat keine anderen Abhängigkeiten als die Python-Standardbibliothek.
Hübsch. Die resultierende Web-App besteht aus <30 LOC und befindet sich in app.py.
Für diese einfache App ist eine einzelne Methode mit einem optionalen Tag-Argument alles, was man braucht. Ähnlich wie bei Flask wird das Routing mit Decorators gehandhabt. Wenn es mit einem Tag aufgerufen wird, werden die Tipps auf dem Tag gefiltert, andernfalls werden sie alle angezeigt. Der View-Decorator definiert die zu verwendende Vorlage. Wie Flask (und Django) geben wir ein Diktat zur Verwendung in der Vorlage zurück.
@route('/')
@route('/<tag>')
@view('index')
def index(tag=None):
tag = tag or request.query.get('tag') or None
tags = get_hashtags()
tips = get_tips(tag)
return {'search_tag': tag or '',
'tags': tags,
'tips': tips}
Um mit statischen Dateien zu arbeiten, fügen Sie laut Dokumentation dieses Snippet oben nach den Importen hinzu:
@route('/static/<filename:path>')
def send_static(filename):
return static_file(filename, root='static')
Schließlich wollen wir sicherstellen, dass wir nur im Debug-Modus auf localhost laufen, daher der APP_LOCATION
env-Variable, die wir im Projekt-Setup definiert haben:
if os.environ.get('APP_LOCATION') == 'heroku':
run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)))
else:
run(host='localhost', port=8080, debug=True, reloader=True)
Flaschenvorlagen
Bottle wird mit einer schnellen, leistungsstarken und einfach zu erlernenden integrierten Vorlagen-Engine namens SimpleTemplate geliefert.
Im Unterverzeichnis views habe ich eine header.tpl definiert , index.tpl , und footer.tpl . Für die Tag-Cloud habe ich ein einfaches Inline-CSS verwendet, um die Tag-Größe durch Anzahl zu erhöhen, siehe header.tpl :
% for tag in tags:
<a style="font-size: {{ tag.count/10 + 1 }}em;" href="/{{ tag.name }}">#{{ tag.name }}</a>
% end
In index.tpl Wir durchlaufen die Tipps:
% for tip in tips:
<div class='tip'>
<pre>{{ !tip.text }}</pre>
<div class="mui--text-dark-secondary"><strong>{{ tip.likes }}</strong> Likes / <strong>{{ tip.retweets }}</strong> RTs / {{ tip.created }} / <a href="https://twitter.com/python_tip/status/{{ tip.tweetid }}" target="_blank">Share</a></div>
</div>
% end
Wenn Sie mit Flask und Jinja2 vertraut sind, sollte Ihnen dies sehr bekannt vorkommen. Das Einbetten von Python ist sogar noch einfacher, mit weniger Eingabeaufwand – (% ...
vs {% ... %}
).
Alle CSS, Bilder (und JS, wenn wir es verwenden würden) gehen in den statischen Unterordner.
Und das ist alles, um mit Bottle eine einfache Web-App zu erstellen. Sobald Sie die Datenschicht richtig definiert haben, ist es ziemlich einfach.
Tests mit pytest hinzufügen
Lassen Sie uns dieses Projekt nun etwas robuster machen, indem wir einige Tests hinzufügen. Das Testen der DB erforderte etwas mehr Einarbeitung in das pytest-Framework, aber am Ende verwendete ich den pytest.fixture-Decorator, um eine Datenbank mit einigen Test-Tweets einzurichten und zu löschen.
Anstatt die Twitter-API aufzurufen, habe ich einige statische Daten aus tweets.json verwendet .Und, anstatt die Live-DB zu verwenden, in tips/db.py , überprüfe ich, ob pytest der Aufrufer ist (sys.argv[0]
). Wenn ja, verwende ich die Test-DB. Ich werde dies wahrscheinlich umgestalten, da Bottle das Arbeiten mit Konfigurationsdateien unterstützt.
Der Hashtag-Teil war einfacher zu testen (test_get_hashtag_counter
), weil ich einer mehrzeiligen Zeichenfolge einfach einige Hashtags hinzufügen könnte. Keine Vorrichtungen erforderlich.
Codequalität ist wichtig – Better Code Hub
Better Code Hub führt Sie beim Schreiben von besserem Code. Vor dem Schreiben der Tests erzielte das Projekt eine 7:
Nicht schlecht, aber wir können es besser machen:
-
Ich habe es auf 9 erhöht, indem ich den Code modularer gestaltete, die DB-Logik aus der app.py (Web-App) entfernte und sie in den Tips-Ordner/das Paket legte (Refaktorisierungen 1 und 2)
-
Mit den durchgeführten Tests erzielte das Projekt dann eine 10:
Schlussfolgerung und Lernen
Unsere Code Challenge Nr. 40 bot einige bewährte Verfahren:
- Ich habe eine nützliche App erstellt, die erweitert werden kann (ich möchte eine API hinzufügen).
- Ich habe einige coole Module verwendet, die es wert sind, erkundet zu werden:Tweepy, SQLAlchemy und Bottle.
- Ich habe etwas mehr über Pytest gelernt, weil ich Fixtures brauchte, um die Interaktion mit der DB zu testen.
- Vor allem, weil der Code testbar gemacht werden musste, wurde die App modularer, was die Wartung erleichterte. Better Code Hub war dabei eine große Hilfe.
- Ich habe die App mithilfe unserer Schritt-für-Schritt-Anleitung für Heroku bereitgestellt.
Wir fordern Sie heraus
Der beste Weg, um Ihre Programmierkenntnisse zu erlernen und zu verbessern, ist das Üben. Bei PyBites haben wir dieses Konzept gefestigt, indem wir Python-Code-Herausforderungen organisiert haben. Sehen Sie sich unsere wachsende Sammlung an, verzweigen Sie das Repo und beginnen Sie mit dem Programmieren!
Teilen Sie uns mit, wenn Sie etwas Cooles bauen, indem Sie einen Pull-Request Ihrer Arbeit stellen. Wir haben gesehen, wie Leute sich wirklich durch diese Herausforderungen gekämpft haben, und wir auch.
Viel Spaß beim Programmieren!