diff --git a/.env.sample b/.env.sample
new file mode 100644
index 0000000..31b8633
--- /dev/null
+++ b/.env.sample
@@ -0,0 +1,5 @@
+TEST_DATABASE_URL=
+TEST_CONSUMER_KEY=
+TEST_CONSUMER_SECRET=
+TEST_ACCESS_TOKEN=
+TEST_ACCESS_TOKEN_SECRET=
diff --git a/.travis.yml b/.travis.yml
index 30d011b..ef8f266 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -11,7 +11,10 @@ script: py.test
before_install:
- sudo apt-get -y update
- sudo apt-get install firefox-geckodriver
- - sudo apt-get install --upgrade chromium-chromedriver
+before_script:
+ - wget https://chromedriver.storage.googleapis.com/83.0.4103.39/chromedriver_linux64.zip
+ - unzip chromedriver_linux64.zip -d /home/travis/virtualenv/python3.7.1/bin/
+ - export CHROME_BIN=chromium-browser
after_failure: cat test/diffengine.log
notifications:
slack:
diff --git a/README.md b/README.md
index 9470b9c..31a3484 100644
--- a/README.md
+++ b/README.md
@@ -88,30 +88,31 @@ Logs can be found in `diffengine.log` in the storage directory, for example
Checkout [Ryan Baumann's "diffengine" Twitter list] for a list of known
diffengine Twitter accounts that are out there.
-## Tweeting text options
+## Config options
-By default, the tweeted diff will include the article's title and the archive diff url, [like this](https://twitter.com/mp_diff/status/1255973684994625539).
+### Database engine
-You change this by tweeting what's changed: the url, the title and/or the summary. For doing so, you need to specify **all** the following `lang` keys:
+By default the database is configured for Sqlite and the file `./diffengine.db` through the `db` config prop
```yaml
-lang:
- change_in: "Change in"
- the_url: "the URL"
- the_title: "the title"
- and: "and"
- the_summary: "the summary"
+db: sqlite:///diffengine.db
```
-Only if all the keys are defined, the tweet will include what's changed on its content, followed by the `diff.url`. Some examples:
+This value responds to the [database URL connection string format](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#database-url).
-- "Change in the title"
-- "Change in the summary"
-- "Change in the title and the summary"
+For instance, you can co˚nnect to your postgresql database using something like this.
-And so on with all the possible combinations between url, title and summary
+```yaml
+db: postgresql://postgres:my_password@localhost:5432/my_database
+```
+
+In case you store your database url connection into an environment var, like in Heroku. You can simply do as follows.
-## Multiple Accounts & Feed Implementation Example
+```yaml
+db: "${DATABASE_URL}"
+```
+
+### Multiple Accounts & Feed Implementation Example
If you are setting multiple accounts, and multiple feeds if may be helpful to setup a
directory for each account. For example:
@@ -155,6 +156,29 @@ twitter:
consumer_secret: CONSUMER_SECRET
```
+### Tweet content
+
+By default, the tweeted diff will include the article's title and the archive diff url, [like this](https://twitter.com/mp_diff/status/1255973684994625539).
+
+You change this by tweeting what's changed: the url, the title and/or the summary. For doing so, you need to specify **all** the following `lang` keys:
+
+```yaml
+lang:
+ change_in: "Change in"
+ the_url: "the URL"
+ the_title: "the title"
+ and: "and"
+ the_summary: "the summary"
+```
+
+Only if all the keys are defined, the tweet will include what's changed on its content, followed by the `diff.url`. Some examples:
+
+- "Change in the title"
+- "Change in the summary"
+- "Change in the title and the summary"
+
+And so on with all the possible combinations between url, title and summary
+
### Support for environment vars
The configuration file has support for [environment variables](https://medium.com/chingu/an-introduction-to-environment-variables-and-how-to-use-them-f602f66d15fa). This is useful if you want to keeping your credentials secure when deploying to Heroku, Vercel (former ZEIT Now), AWS, Azure, Google Cloud or any other similar services. The environment variables are defined on the app of the platform you use or directly in a [dotenv file](https://12factor.net/config), which is the usual case when coding locally.
@@ -176,7 +200,7 @@ MY_CONSUMER_SECRET_ENV_VAR="CONSUMER_SECRET"
Done! You can use diffengine as usual and keep your credentials safe.
-## Adding a Twitter account when the configuration file is already created
+### Adding a Twitter account when the configuration file is already created
You can use the following command for adding Twitter accounts to the config file.
diff --git a/config-test.yaml b/config-test.yaml
new file mode 100644
index 0000000..3fefc3b
--- /dev/null
+++ b/config-test.yaml
@@ -0,0 +1,7 @@
+db: ${TEST_DATABASE_URL}
+twitter:
+ consumer_key: ${TEST_CONSUMER_KEY}
+ consumer_secret: ${TEST_CONSUMER_SECRET}
+ token:
+ access_token: ${TEST_ACCESS_TOKEN}
+ access_token_secret: ${TEST_ACCESS_TOKEN_SECRET}
diff --git a/diffengine/__init__.py b/diffengine/__init__.py
index c0c155b..05927f7 100755
--- a/diffengine/__init__.py
+++ b/diffengine/__init__.py
@@ -8,7 +8,6 @@
import os
import re
import sys
-import json
import time
import yaml
import bleach
@@ -19,46 +18,50 @@
import logging
import argparse
import requests
-import selenium
import htmldiff2
import feedparser
-import subprocess
import readability
import unicodedata
-from peewee import *
-from playhouse.migrate import SqliteMigrator, migrate
from datetime import datetime
-from selenium import webdriver
-from selenium.webdriver.chrome.options import Options as ChromeOptions
-from selenium.webdriver.firefox.options import Options as FirefoxOptions
-from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
-from envyaml import EnvYAML
-
from diffengine.exceptions.webdriver import UnknownWebdriverError
-from diffengine.exceptions.twitter import ConfigNotFoundError, TwitterError
+from diffengine.exceptions.sendgrid import SendgridConfigNotFoundError, SendgridError
+from diffengine.exceptions.twitter import TwitterConfigNotFoundError, TwitterError
from diffengine.text import to_utf8
+from diffengine.sendgrid import SendgridHandler
from diffengine.twitter import TwitterHandler
-from diffengine.exceptions.sendgrid import (
- ConfigNotFoundError as SGConfigNotFoundError,
- SendgridError,
+from envyaml import EnvYAML
+from peewee import (
+ DatabaseProxy,
+ CharField,
+ DateTimeField,
+ OperationalError,
+ ForeignKeyField,
+ Model,
+ SqliteDatabase,
+ TextField,
)
-from diffengine.sendgrid import SendgridHandler
+from playhouse.db_url import connect
+from playhouse.migrate import SqliteMigrator, migrate
+from selenium import webdriver
+from selenium.webdriver.chrome.options import Options as ChromeOptions
+from selenium.webdriver.firefox.options import Options as FirefoxOptions
+from urllib.parse import urlparse, urlunparse, parse_qs, urlencode
home = None
config = {}
-db = SqliteDatabase(None)
+database = DatabaseProxy()
browser = None
class BaseModel(Model):
class Meta:
- database = db
+ database = database
class Feed(BaseModel):
- url = CharField(primary_key=True)
- name = CharField()
+ url = TextField(primary_key=True)
+ name = TextField()
created = DateTimeField(default=datetime.utcnow)
@property
@@ -102,7 +105,7 @@ def get_latest(self):
class Entry(BaseModel):
- url = CharField()
+ url = TextField()
created = DateTimeField(default=datetime.utcnow)
checked = DateTimeField(default=datetime.utcnow)
tweet_status_id_str = CharField(null=False, default="")
@@ -154,9 +157,9 @@ def get_latest(self):
be returned.
"""
- # make sure we don't go too fast
- # TODO: can we remove this? Why is this here?
- time.sleep(1)
+ time_sleep = config.get("time_sleep", 0)
+ if time_sleep > 0:
+ time.sleep(time_sleep)
# fetch the current readability-ized content for the page
logging.info("checking %s", self.url)
@@ -231,11 +234,11 @@ class FeedEntry(BaseModel):
class EntryVersion(BaseModel):
- title = CharField()
- url = CharField(index=True)
- summary = CharField()
+ title = TextField()
+ url = TextField(index=True)
+ summary = TextField()
created = DateTimeField(default=datetime.utcnow)
- archive_url = CharField(null=True)
+ archive_url = TextField(null=True)
entry = ForeignKeyField(Entry, backref="versions")
tweet_status_id_str = CharField(null=False, default="")
@@ -299,6 +302,18 @@ class Diff(BaseModel):
emailed = DateTimeField(null=True)
blogged = DateTimeField(null=True)
+ @property
+ def url_changed(self):
+ return self.old.url != self.new.url
+
+ @property
+ def title_changed(self):
+ return self.old.title != self.new.title
+
+ @property
+ def summary_changed(self):
+ return self.old.summary != self.new.summary
+
@property
def html_path(self):
# use prime number to spread across directories
@@ -485,17 +500,20 @@ def home_path(rel_path):
def setup_db():
- global db
- db_file = config.get("db", home_path("diffengine.db"))
- logging.debug("connecting to db %s", db_file)
- db.init(db_file)
- db.connect()
- db.create_tables([Feed, Entry, FeedEntry, EntryVersion, Diff], safe=True)
- try:
- migrator = SqliteMigrator(db)
- migrate(migrator.add_index("entryversion", ("url",), False))
- except OperationalError as e:
- logging.debug(e)
+ global home, database
+ database_url = config.get("db", "sqlite:///diffengine.db")
+ logging.debug("connecting to db %s", database_url)
+ database_handler = connect(database_url)
+ database.initialize(database_handler)
+ database.connect()
+ database.create_tables([Feed, Entry, FeedEntry, EntryVersion, Diff], safe=True)
+
+ if isinstance(database_handler, SqliteDatabase):
+ try:
+ migrator = SqliteMigrator(database_handler)
+ migrate(migrator.add_index("entryversion", ("url",), False))
+ except OperationalError as e:
+ logging.debug(e)
def chromedriver_browser(executable_path, binary_location):
@@ -532,7 +550,7 @@ def setup_browser(engine="geckodriver", executable_path=None, binary_location=""
def init(new_home, prompt=True):
- global home, browser
+ global home, config, browser
home = new_home
load_config(prompt)
try:
@@ -565,7 +583,7 @@ def main():
twitter_handler = TwitterHandler(
twitter_config["consumer_key"], twitter_config["consumer_secret"]
)
- except ConfigNotFoundError as e:
+ except TwitterConfigNotFoundError as e:
twitter_handler = None
logging.warning("error when creating Twitter Handler. Reason", str(e))
except KeyError as e:
@@ -629,7 +647,7 @@ def process_entry(entry, feed_config, twitter=None, sendgrid=None, lang={}):
version.diff, feed_config.get("sendgrid", {})
)
- except SGConfigNotFoundError as e:
+ except SendgridConfigNotFoundError as e:
logging.error(
"Missing configuration values for publishing entry %s",
entry.url,
diff --git a/diffengine/exceptions/sendgrid.py b/diffengine/exceptions/sendgrid.py
index 1b95567..a7118e3 100644
--- a/diffengine/exceptions/sendgrid.py
+++ b/diffengine/exceptions/sendgrid.py
@@ -2,7 +2,7 @@ class SendgridError(RuntimeError):
pass
-class ConfigNotFoundError(SendgridError):
+class SendgridConfigNotFoundError(SendgridError):
"""Exception raised if the Sendgrid instance has not the API key"""
def __init__(self):
@@ -14,6 +14,6 @@ def __init__(self, diff_id):
self.message = "diff %s was already emailed with sendgrid " % diff_id
-class ArchiveUrlNotFoundError(SendgridError):
+class SendgridArchiveUrlNotFoundError(SendgridError):
def __init__(self):
self.message = "not publishing without archive urls"
diff --git a/diffengine/exceptions/twitter.py b/diffengine/exceptions/twitter.py
index 81f71c8..112c006 100644
--- a/diffengine/exceptions/twitter.py
+++ b/diffengine/exceptions/twitter.py
@@ -2,7 +2,7 @@ class TwitterError(RuntimeError):
pass
-class ConfigNotFoundError(TwitterError):
+class TwitterConfigNotFoundError(TwitterError):
"""Exception raised if the Twitter instance has not the required key and secret"""
def __init__(self):
@@ -21,7 +21,7 @@ def __init__(self, diff):
self.message = "diff %s has already been tweeted" % diff.id
-class AchiveUrlNotFoundError(TwitterError):
+class TwitterAchiveUrlNotFoundError(TwitterError):
def __init__(self, diff):
self.message = "not tweeting without archive urls for diff %s" % diff.id
diff --git a/diffengine/sendgrid.py b/diffengine/sendgrid.py
index c382f68..640a180 100644
--- a/diffengine/sendgrid.py
+++ b/diffengine/sendgrid.py
@@ -5,8 +5,8 @@
from diffengine.exceptions.sendgrid import (
AlreadyEmailedError,
- ConfigNotFoundError,
- ArchiveUrlNotFoundError,
+ SendgridConfigNotFoundError,
+ SendgridArchiveUrlNotFoundError,
)
@@ -43,13 +43,13 @@ def publish_diff(self, diff, feed_config):
if diff.emailed:
raise AlreadyEmailedError(diff.id)
elif not (diff.old.archive_url and diff.new.archive_url):
- raise ArchiveUrlNotFoundError()
+ raise SendgridArchiveUrlNotFoundError()
api_token = feed_config.get("api_token", self.api_token)
sender = feed_config.get("sender", self.sender)
receivers = feed_config.get("receivers", self.receivers)
if not all([api_token, sender, receivers]):
- raise ConfigNotFoundError
+ raise SendgridConfigNotFoundError
subject = self.build_subject(diff)
message = Mail(
diff --git a/diffengine/twitter.py b/diffengine/twitter.py
index 702a8ee..5447a4f 100644
--- a/diffengine/twitter.py
+++ b/diffengine/twitter.py
@@ -6,9 +6,9 @@
from diffengine.text import build_text
from diffengine.exceptions.twitter import (
AlreadyTweetedError,
- ConfigNotFoundError,
+ TwitterConfigNotFoundError,
TokenNotFoundError,
- AchiveUrlNotFoundError,
+ TwitterAchiveUrlNotFoundError,
UpdateStatusError,
)
@@ -19,7 +19,7 @@ class TwitterHandler:
def __init__(self, consumer_key, consumer_secret):
if not consumer_key or not consumer_secret:
- raise ConfigNotFoundError()
+ raise TwitterConfigNotFoundError()
self.consumer_key = consumer_key
self.consumer_secret = consumer_secret
@@ -59,14 +59,14 @@ def tweet_diff(self, diff, token=None, lang={}):
elif diff.tweeted:
raise AlreadyTweetedError(diff)
elif not (diff.old.archive_url and diff.new.archive_url):
- raise AchiveUrlNotFoundError(diff)
+ raise TwitterAchiveUrlNotFoundError(diff)
twitter = self.api(token)
text = build_text(diff, lang)
# Check if the thread exists
thread_status_id_str = None
- if diff.old.entry.tweet_status_id_str is None:
+ if diff.old.entry.tweet_status_id_str == "":
try:
thread_status_id_str = self.create_thread(
diff.old.entry, diff.old, token
@@ -98,3 +98,8 @@ def tweet_diff(self, diff, token=None, lang={}):
diff.save()
except Exception as e:
logging.error("unable to tweet: %s", e)
+
+ def delete_diff(self, diff, token=None):
+ twitter = self.api(token)
+ twitter.destroy_status(diff.old.tweet_status_id_str)
+ twitter.destroy_status(diff.new.tweet_status_id_str)
diff --git a/diffengine/utils.py b/diffengine/utils.py
new file mode 100644
index 0000000..59372d3
--- /dev/null
+++ b/diffengine/utils.py
@@ -0,0 +1,11 @@
+import os
+import yaml
+
+
+def generate_config(home, content):
+ config_file = os.path.join(home, "config.yaml")
+
+ if not os.path.isdir(home):
+ os.makedirs(home)
+
+ yaml.dump(content, open(config_file, "w"), default_flow_style=False)
diff --git a/requirements.txt b/requirements.txt
index 7d1cd07..9cced30 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -14,3 +14,4 @@ readability-lxml
envyaml>=0.1912
pre-commit==2.3.0
sendgrid
+psycopg2-binary==2.8.5
diff --git a/test_diffengine.py b/test_diffengine.py
index e01b1a5..b166e24 100644
--- a/test_diffengine.py
+++ b/test_diffengine.py
@@ -1,14 +1,14 @@
import logging
import os
import re
-
import yaml
-from selenium import webdriver
+from envyaml import EnvYAML
import setup
import pytest
import shutil
+from selenium import webdriver
from unittest import TestCase
from unittest.mock import MagicMock, patch
from unittest.mock import PropertyMock
@@ -27,147 +27,178 @@
UA,
TwitterHandler,
SendgridHandler,
+ _fingerprint,
)
+from diffengine.text import build_text, to_utf8
+from diffengine.utils import generate_config
from diffengine.exceptions.sendgrid import (
- ConfigNotFoundError as SGConfigNotFoundError,
- AlreadyEmailedError as SGAlreadyEmailedError,
- ArchiveUrlNotFoundError as SGArchiveNotFoundError,
+ SendgridConfigNotFoundError,
+ AlreadyEmailedError,
+ SendgridArchiveUrlNotFoundError,
)
-from diffengine.text import build_text
from diffengine.exceptions.twitter import (
- ConfigNotFoundError,
+ TwitterConfigNotFoundError,
TokenNotFoundError,
AlreadyTweetedError,
- AchiveUrlNotFoundError,
+ TwitterAchiveUrlNotFoundError,
UpdateStatusError,
)
-if os.path.isdir("test"):
- shutil.rmtree("test")
-
-# set things up but disable prompting for initial feed
-init("test", prompt=False)
+test_home = "test"
+test_env_file = ".env"
+test_config = EnvYAML(
+ "config-test.yaml",
+ env_file=test_env_file if os.path.isfile(test_env_file) else None,
+)
-# the sequence of these tests is significant
+if os.path.isdir(test_home):
+ shutil.rmtree(test_home)
def test_version():
assert setup.version in UA
-def test_feed():
- f = Feed.create(name="Test", url="https://inkdroid.org/feed.xml")
- f.get_latest()
- assert f.created
- assert len(f.entries) == 10
-
-
-def test_entry():
- f = Feed.get(Feed.url == "https://inkdroid.org/feed.xml")
- e = f.entries[0]
- v = e.get_latest()
- assert type(v) == EntryVersion
- assert len(e.versions) == 1
-
-
-def test_diff():
- f = Feed.get(Feed.url == "https://inkdroid.org/feed.xml")
- e = f.entries[0]
- v1 = e.versions[0]
-
- # remove some characters from the version
- v1.summary = v1.summary[0:-20]
- v1.save()
-
- v2 = e.get_latest()
- assert type(v2) == EntryVersion
- assert v2.diff
- assert v2.archive_url is not None
- assert (
- re.match("^https://web.archive.org/web/[0-9]+/.+$", v2.archive_url) is not None
- )
-
- diff = v2.diff
- assert diff.old == v1
- assert diff.new == v2
- assert os.path.isfile(diff.html_path)
- assert os.path.isfile(diff.screenshot_path)
- assert os.path.isfile(diff.thumbnail_path)
-
- # check that the url for the internet archive diff is working
- assert re.match("^https://web.archive.org/web/diff/\d+/\d+/https.+$", diff.url)
-
-
-def test_html_diff():
- f = Feed.get(Feed.url == "https://inkdroid.org/feed.xml")
- e = f.entries[0]
-
- # add a change to the summary that htmldiff ignores
- v1 = e.versions[-1]
- parts = v1.summary.split()
- parts.insert(2, "
\n")
- v1.summary = " ".join(parts)
- v1.save()
-
- v2 = e.get_latest()
- assert v2 is None
-
-
-def test_many_to_many():
-
- # these two feeds share this entry, we want diffengine to support
- # multiple feeds for the same content, which is fairly common at
- # large media organizations with multiple topical feeds
- url = "https://www.washingtonpost.com/classic-apps/how-a-week-of-tweets-by-trump-stoked-anxiety-moved-markets-and-altered-plans/2017/01/07/38be8e64-d436-11e6-9cb0-54ab630851e8_story.html"
-
- f1 = Feed.create(
- name="feed1",
- url="https://raw.githubusercontent.com/DocNow/diffengine/master/test-data/feed1.xml",
- )
- f1.get_latest()
+def test_fingerprint():
+ assert _fingerprint("foo bar") == "foobar"
+ assert _fingerprint("foo bar\nbaz") == "foobarbaz"
+ assert _fingerprint("foo
bar") == "foobar"
+ assert _fingerprint("foo'bar") == "foobar"
+ assert _fingerprint("foo’bar") == "foobar"
- f2 = Feed.create(
- name="feed2",
- url="https://raw.githubusercontent.com/DocNow/diffengine/master/test-data/feed2.xml",
- )
- f2.get_latest()
- assert f1.entries.where(Entry.url == url).count() == 1
- assert f2.entries.where(Entry.url == url).count() == 1
+class FeedTest(TestCase):
+ feed = None
+ entry = None
+ version = None
- e = Entry.get(Entry.url == url)
- assert FeedEntry.select().where(FeedEntry.entry == e).count() == 2
+ def setUp(self) -> None:
+ generate_config(test_home, {"db": test_config.get("db", "sqlite:///:memory:")})
+ # set things up but disable prompting for initial feed
+ init(test_home, prompt=False)
+ self.feed = Feed.create(name="Test", url="https://inkdroid.org/feed.xml")
+ self.feed.get_latest()
+ self.entry = self.feed.entries[0]
+ self.version = self.entry.get_latest()
+
+ def test_feed(self):
+ assert self.feed.created
+ assert len(self.feed.entries) == 10
+
+ def test_entry(self):
+ assert type(self.version) == EntryVersion
+ assert len(self.entry.versions) == 1
+
+ def test_diff(self):
+ e = self.entry
+ v1 = e.versions[0]
+
+ # remove some characters from the version
+ v1.summary = v1.summary[0:-20]
+ v1.save()
+
+ v2 = e.get_latest()
+ assert type(v2) == EntryVersion
+ assert v2.diff
+ assert v2.archive_url is not None
+ assert (
+ re.match("^https://web.archive.org/web/[0-9]+/.+$", v2.archive_url)
+ is not None
+ )
+ diff = v2.diff
+ assert diff.old == v1
+ assert diff.new == v2
+ assert os.path.isfile(diff.html_path)
+ assert os.path.isfile(diff.screenshot_path)
+ assert os.path.isfile(diff.thumbnail_path)
-def test_bad_feed_url():
- # bad feed url shouldn't cause a fatal exception
- f = Feed.create(name="feed1", url="http://example.org/feedfeed.xml")
- f.get_latest()
- assert True
+ # check that the url for the internet archive diff is working
+ assert re.match(
+ "^https://web.archive.org/web/diff/\\d+/\\d+/https.+$", diff.url
+ )
+ def test_html_diff(self):
+ e = self.entry
-def test_whitespace():
- f = Feed.get(url="https://inkdroid.org/feed.xml")
- e = f.entries[0]
- v1 = e.versions[-1]
+ # add a change to the summary that htmldiff ignores
+ v1 = e.versions[-1]
+ parts = v1.summary.split()
+ parts.insert(2, "
\n")
+ v1.summary = " ".join(parts)
+ v1.save()
- # add some whitespace
- v1.summary = v1.summary + "\n\n "
- v1.save()
+ v2 = e.get_latest()
+ assert v2 is None
- # whitespace should not count when diffing
- v2 = e.get_latest()
- assert v2 == None
+ def test_many_to_many(self):
+ # these two feeds share this entry, we want diffengine to support
+ # multiple feeds for the same content, which is fairly common at
+ # large media organizations with multiple topical feeds
+ url = "https://www.washingtonpost.com/classic-apps/how-a-week-of-tweets-by-trump-stoked-anxiety-moved-markets-and-altered-plans/2017/01/07/38be8e64-d436-11e6-9cb0-54ab630851e8_story.html"
-def test_fingerprint():
- from diffengine import _fingerprint
+ f1 = Feed.create(
+ name="feed1",
+ url="https://raw.githubusercontent.com/DocNow/diffengine/master/test-data/feed1.xml",
+ )
+ f1.get_latest()
- assert _fingerprint("foo bar") == "foobar"
- assert _fingerprint("foo bar\nbaz") == "foobarbaz"
- assert _fingerprint("foo
bar") == "foobar"
- assert _fingerprint("foo'bar") == "foobar"
- assert _fingerprint("foo’bar") == "foobar"
+ f2 = Feed.create(
+ name="feed2",
+ url="https://raw.githubusercontent.com/DocNow/diffengine/master/test-data/feed2.xml",
+ )
+ f2.get_latest()
+
+ assert f1.entries.where(Entry.url == url).count() == 1
+ assert f2.entries.where(Entry.url == url).count() == 1
+
+ e = Entry.get(Entry.url == url)
+ assert FeedEntry.select().where(FeedEntry.entry == e).count() == 2
+
+ def test_bad_feed_url(self):
+ # bad feed url shouldn't cause a fatal exception
+ f = Feed.create(name="feed1", url="http://example.org/feedfeed.xml")
+ f.get_latest()
+ assert True
+
+ def test_whitespace(self):
+ e = self.feed.entries[0]
+ v1 = e.versions[-1]
+
+ # add some whitespace
+ v1.summary = v1.summary + "\n\n "
+ v1.save()
+
+ # whitespace should not count when diffing
+ v2 = e.get_latest()
+ assert v2 == None
+
+ # This one is only for tweeting purposes only
+ # If no .env var is set, this one will success anyway :)
+ def test_tweet_diff(self):
+ e = self.entry
+ v1 = e.versions[0]
+
+ # remove some characters from the version
+ v1.summary = v1.summary[0:-20]
+ v1.save()
+
+ v2 = e.get_latest()
+
+ # run this alone for checking correct tweeting behavior
+ if v2 is not None:
+ diff = v2.diff
+ try:
+ token = test_config.get("twitter.token")
+ twitter_handler = TwitterHandler(
+ test_config.get("twitter.consumer_key"),
+ test_config.get("twitter.consumer_secret"),
+ )
+ twitter_handler.tweet_diff(diff, token)
+ twitter_handler.delete_diff(diff, token)
+ except Exception:
+ logging.debug("no tweet configured for test. Doing nothing")
class EnvVarsTest(TestCase):
@@ -186,8 +217,7 @@ def test_config_file_integration(self):
test_config = {
"example": {"private_value": private_yaml_key, "public_value": public_value}
}
- config_file = home_path("config.yaml")
- yaml.dump(test_config, open(config_file, "w"), default_flow_style=False)
+ generate_config(test_home, test_config)
# test!
new_config = load_config()
@@ -367,13 +397,17 @@ def tearDown(self) -> None:
logging.disable(logging.NOTSET)
def test_raises_if_no_config_set(self):
- self.assertRaises(ConfigNotFoundError, TwitterHandler, None, None)
- self.assertRaises(ConfigNotFoundError, TwitterHandler, "myConsumerKey", None)
- self.assertRaises(ConfigNotFoundError, TwitterHandler, None, "myConsumerSecret")
+ self.assertRaises(TwitterConfigNotFoundError, TwitterHandler, None, None)
+ self.assertRaises(
+ TwitterConfigNotFoundError, TwitterHandler, "myConsumerKey", None
+ )
+ self.assertRaises(
+ TwitterConfigNotFoundError, TwitterHandler, None, "myConsumerSecret"
+ )
try:
TwitterHandler("myConsumerKey", "myConsumerSecret")
- except ConfigNotFoundError:
+ except TwitterConfigNotFoundError:
self.fail("Twitter.__init__ raised ConfigNotFoundError unexpectedly!")
def test_raises_if_no_token_provided(self):
@@ -401,15 +435,19 @@ def test_raises_if_not_all_archive_urls_are_present(self):
}
twitter = TwitterHandler("myConsumerKey", "myConsumerSecret")
- self.assertRaises(AchiveUrlNotFoundError, twitter.tweet_diff, diff, token)
+ self.assertRaises(
+ TwitterAchiveUrlNotFoundError, twitter.tweet_diff, diff, token
+ )
type(diff.old).archive_url = PropertyMock(return_value="http://test.url/old")
- self.assertRaises(AchiveUrlNotFoundError, twitter.tweet_diff, diff, token)
+ self.assertRaises(
+ TwitterAchiveUrlNotFoundError, twitter.tweet_diff, diff, token
+ )
type(diff.new).archive_url = PropertyMock(return_value="http://test.url/new")
try:
twitter.tweet_diff(diff, token)
- except AchiveUrlNotFoundError:
+ except TwitterAchiveUrlNotFoundError:
self.fail("twitter.tweet_diff raised AchiveUrlNotFoundError unexpectedly!")
class MockedStatus(MagicMock):
@@ -423,7 +461,7 @@ def test_create_thread_if_old_entry_has_no_related_tweet(
):
entry = MagicMock()
- type(entry).tweet_status_id_str = PropertyMock(return_value=None)
+ type(entry).tweet_status_id_str = PropertyMock(return_value="")
diff = get_mocked_diff()
type(diff.old).entry = entry
@@ -560,10 +598,10 @@ def test_raises_if_no_config_set(self):
type(diff).emailed = PropertyMock(return_value=False)
sendgrid = SendgridHandler({})
- self.assertRaises(SGConfigNotFoundError, sendgrid.publish_diff, diff, {})
+ self.assertRaises(SendgridConfigNotFoundError, sendgrid.publish_diff, diff, {})
try:
sendgrid.publish_diff(diff, self.config["sendgrid"])
- except SGConfigNotFoundError:
+ except SendgridConfigNotFoundError:
self.fail("sendgrid.publish_diff raised ConfigNotFoundError unexpectedly!")
def test_raises_if_already_emailed(self):
@@ -572,7 +610,7 @@ def test_raises_if_already_emailed(self):
sendgrid = SendgridHandler(self.config["sendgrid"])
self.assertRaises(
- SGAlreadyEmailedError, sendgrid.publish_diff, diff, self.config["sendgrid"]
+ AlreadyEmailedError, sendgrid.publish_diff, diff, self.config["sendgrid"]
)
def test_raises_if_not_all_archive_urls_are_present(self):
@@ -580,18 +618,24 @@ def test_raises_if_not_all_archive_urls_are_present(self):
sendgrid = SendgridHandler(self.config["sendgrid"])
self.assertRaises(
- SGArchiveNotFoundError, sendgrid.publish_diff, diff, self.config["sendgrid"]
+ SendgridArchiveUrlNotFoundError,
+ sendgrid.publish_diff,
+ diff,
+ self.config["sendgrid"],
)
type(diff.old).archive_url = PropertyMock(return_value="http://test.url/old")
self.assertRaises(
- SGArchiveNotFoundError, sendgrid.publish_diff, diff, self.config["sendgrid"]
+ SendgridArchiveUrlNotFoundError,
+ sendgrid.publish_diff,
+ diff,
+ self.config["sendgrid"],
)
type(diff.new).archive_url = PropertyMock(return_value="http://test.url/new")
try:
sendgrid.publish_diff(diff, self.config["sendgrid"])
- except SGArchiveNotFoundError:
+ except SendgridArchiveUrlNotFoundError:
self.fail(
"sendgrid.publish_diff raised AchiveUrlNotFoundError unexpectedly!"
)