diff --git a/.codeclimate.yml b/.codeclimate.yml index 1c45168..78bf5f2 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -2,10 +2,10 @@ version: "2" checks: argument-count: config: - threshold: 5 + threshold: 7 method-complexity: config: - threshold: 7 + threshold: 15 plugins: duplication: enabled: true @@ -20,17 +20,5 @@ plugins: config: threshold: "C" exclude_patterns: - - "config/" - - "db/" - - "dist/" - - "features/" - - "**/node_modules/" - - "script/" - - "**/spec/" - - "**/test/" - "**/tests/" - - "Tests/" - - "**/vendor/" - - "**/*_test.go" - - "**/*.d.ts" - "**/secret_*.py" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 58d5191..548b518 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -26,22 +26,15 @@ jobs: uses: actions/cache@v1 with: path: ~/.cache/pip # This path is specific to Ubuntu - key: ${{ runner.os }}-pip-${{ hashFiles('./requirements.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: Cache CI dependencies - uses: actions/cache@v1 - with: - path: ~/.cache/pip # This path is specific to Ubuntu - key: ${{ runner.os }}-pip-${{ hashFiles('./.github/workflows/requirements/docs.txt') }} + key: ${{ runner.os }}-pip-${{ hashFiles('./requirements/requirements.txt') }}-${{ hashFiles('**/docs.txt') }}-${{ hashFiles('**/test.txt') }} restore-keys: | ${{ runner.os }}-pip- - name: Install dependencies run: | - pip install -r requirements.txt - pip install -r ./.github/workflows/requirements/docs.txt + pip install -r ./requirements/requirements.txt + pip install -r ./requirements/docs.txt + pip install -r ./requirements/test.txt - name: Unlock git-crypt files uses: zemuldo/git-crypt-unlock@v3.0-alpha-1 @@ -50,6 +43,12 @@ jobs: GPG_KEY_GRIP: ${{ secrets.GPG_KEY_GRIP }} GPG_KEY_PASS: ${{ secrets.GPG_KEY_PASS }} + - name: Generate project dependency graphs + run: | + pyreverse --ignore="tests" -o png -p telereddit telereddit + mv classes_telereddit.png docs/images/classes_telereddit.png + mv packages_telereddit.png docs/images/packages_telereddit.png + - name: Generate documentation run: | rm -rf docs/telereddit @@ -57,28 +56,24 @@ jobs: env: TELEREDDIT_MACHINE: GITHUB - - name: Run documentation coverage + - name: Run docstr coverage run: | mkdir documentation-reports - docstr-coverage telereddit --percentage-only --exclude=tests --skipinit 2>&1 | tee ./documentation-reports/docstr-coverage.txt + docstr-coverage telereddit --verbose=3 --exclude=tests --skipinit 2>&1 | tee ./documentation-reports/docstr-coverage.txt + + - name: Run pydocstyle + run: | (pydocstyle telereddit 2>&1 | tee ./documentation-reports/pydocstyle-coverage.txt; exit ${PIPESTATUS[0]}) - name: Upload documentation reports as artifact uses: actions/upload-artifact@v2 with: name: documentation-reports - path: ./documentation-reports/ + path: ./documentation-reports/docstr-coverage.txt - name: Commit changes uses: EndBug/add-and-commit@v4 with: message: "Commit from Github Actions: docs workflow changes" env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Deploy docs to Gtihub Pages - uses: JamesIves/github-pages-deploy-action@releases/v3 - with: - ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} - BRANCH: gh-pages - FOLDER: docs/telereddit + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/github-pages.yml b/.github/workflows/github-pages.yml new file mode 100644 index 0000000..0b52963 --- /dev/null +++ b/.github/workflows/github-pages.yml @@ -0,0 +1,52 @@ +name: github-pages + +on: + push: + branches: + - master + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + + - name: Set up Python 3.8 + uses: actions/setup-python@v1 + with: + python-version: 3.8 + + - name: Cache dependencies + uses: actions/cache@v1 + with: + path: ~/.cache/pip # This path is specific to Ubuntu + key: ${{ runner.os }}-pip-${{ hashFiles('./requirements/requirements.txt') }}-${{ hashFiles('**/docs.txt') }}-${{ hashFiles('**/test.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + pip install -r ./requirements/requirements.txt + pip install -r ./requirements/docs.txt + pip install -r ./requirements/test.txt + + - name: Unlock git-crypt files + uses: zemuldo/git-crypt-unlock@v3.0-alpha-1 + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_KEY_GRIP: ${{ secrets.GPG_KEY_GRIP }} + GPG_KEY_PASS: ${{ secrets.GPG_KEY_PASS }} + + - name: Generate documentation + run: | + rm -rf docs/telereddit + pdoc --html --output-dir="docs/" --template-dir="docs/templates" --config show_source_code=False --force . + env: + TELEREDDIT_MACHINE: GITHUB + + - name: Deploy docs to Github Pages + uses: JamesIves/github-pages-deploy-action@releases/v3 + with: + ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} + BRANCH: gh-pages + FOLDER: docs/telereddit diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8a1485b..09c1d4d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,20 +18,33 @@ jobs: with: python-version: 3.8 - - name: Cache CI dependencies + - name: Cache dependencies uses: actions/cache@v1 with: path: ~/.cache/pip # This path is specific to Ubuntu - key: ${{ runner.os }}-pip-${{ hashFiles('./.github/workflows/requirements/lint.txt') }} + key: ${{ runner.os }}-pip-${{ hashFiles('./requirements/lint.txt') }} restore-keys: | ${{ runner.os }}-pip- - name: Install dependencies run: | - pip install -r ./.github/workflows/requirements/lint.txt + pip install -r ./requirements/lint.txt + + - name: Unlock git-crypt files + uses: zemuldo/git-crypt-unlock@v3.0-alpha-1 + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_KEY_GRIP: ${{ secrets.GPG_KEY_GRIP }} + GPG_KEY_PASS: ${{ secrets.GPG_KEY_PASS }} - name: Check black code formatting run: python -m black telereddit --check - name: Check flake8 linting - run: python -m flake8 --config setup.cfg telereddit \ No newline at end of file + run: python -m flake8 --config setup.cfg telereddit + + - name: Check mypy linting + run: mypy telereddit + + - name: Check icontract linting + run: pyicontract-lint --format verbose telereddit diff --git a/.github/workflows/requirements/docs.txt b/.github/workflows/requirements/docs.txt deleted file mode 100644 index 97350b1..0000000 --- a/.github/workflows/requirements/docs.txt +++ /dev/null @@ -1,3 +0,0 @@ -pdoc3>=0.8.1 -pydocstyle>=5.0.2 -docstr-coverage>=1.0.5 \ No newline at end of file diff --git a/.github/workflows/requirements/lint.txt b/.github/workflows/requirements/lint.txt deleted file mode 100644 index 7e6fb2c..0000000 --- a/.github/workflows/requirements/lint.txt +++ /dev/null @@ -1,2 +0,0 @@ -black>=19.10b0 -flake8>=3.7.9 \ No newline at end of file diff --git a/.github/workflows/requirements/test.txt b/.github/workflows/requirements/test.txt deleted file mode 100644 index e4d3546..0000000 --- a/.github/workflows/requirements/test.txt +++ /dev/null @@ -1,2 +0,0 @@ -coverage>=5.0.4 -coveralls>=2.0.0 \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 127a512..8c2497b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,22 +33,14 @@ jobs: uses: actions/cache@v1 with: path: ~/.cache/pip # This path is specific to Ubuntu - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - - name: Cache CI dependencies - uses: actions/cache@v1 - with: - path: ~/.cache/pip # This path is specific to Ubuntu - key: ${{ runner.os }}-pip-${{ hashFiles('./.github/workflows/requirements/test.txt') }} + key: ${{ runner.os }}-pip-${{ hashFiles('./requirements/requirements.txt') }}-${{ hashFiles('**/test.txt') }} restore-keys: | ${{ runner.os }}-pip- - name: Install dependencies run: | - pip install -r requirements.txt - pip install -r ./.github/workflows/requirements/test.txt + pip install -r ./requirements/requirements.txt + pip install -r ./requirements/test.txt - name: Run tests and generate report run: | diff --git a/.gitignore b/.gitignore index 71a7f1d..20423f6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,6 @@ __pycache__/ *.py[cod] *$py.class -# C extensions -*.so - # Distribution / packaging .Python build/ @@ -25,16 +22,6 @@ wheels/ *.egg MANIFEST -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - # Unit test / coverage reports htmlcov/ .tox/ @@ -47,40 +34,6 @@ coverage.xml .hypothesis/ .pytest_cache/ -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# pyenv -.python-version - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - # Environments .env .venv @@ -103,10 +56,17 @@ venv.bak/ # mypy .mypy_cache/ +.mypy +# IDE .vscode .idea +# Python __pycache__ -documentation-reports \ No newline at end of file +documentation-reports + +html +.mutmut-cache +.metadata diff --git a/README.md b/README.md index e6ce9a7..2392eca 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,14 @@ Get it on [telegram.me](https://telegram.me/tele_reddit_bot)! ## Bugs and feature requests If you want to report a bug or would like a feature to be added, feel free to open an issue. + + +## Versioning +We follow Semantic Versioning. The version X.Y.Z indicates: + +* X is the major version (backward-incompatible), +* Y is the minor version (backward-compatible), and +* Z is the patch version (backward-compatible bug fix). ## License **[GPL v3](https://www.gnu.org/licenses/gpl-3.0)** - Copyright 2020 © - - - - - -G - - - -parameterized - -parameterized - - - -telereddit_tests_test_helpers - -telereddit. -tests. -test_helpers - - - -parameterized->telereddit_tests_test_helpers - - - - - - -telereddit_tests_test_post - -telereddit. -tests. -test_post - - - -parameterized->telereddit_tests_test_post - - - - - -telereddit_tests_test_reddit - -telereddit. -tests. -test_reddit - - - -parameterized->telereddit_tests_test_reddit - - - - -telereddit_tests_test_services - -telereddit. -tests. -test_services - - - -parameterized->telereddit_tests_test_services - - - - - - -parameterized_parameterized - -parameterized. -parameterized - - - -parameterized_parameterized->parameterized - - - - - -parameterized_parameterized->telereddit_tests_test_helpers - - - - - -parameterized_parameterized->telereddit_tests_test_post - - - - - -parameterized_parameterized->telereddit_tests_test_reddit - - - - - - - -parameterized_parameterized->telereddit_tests_test_services - - - - - - -requests - -requests - - - -telereddit_helpers - -telereddit. -helpers - - - -requests->telereddit_helpers - - - - - - -telereddit_reddit - -telereddit. -reddit - - - -requests->telereddit_reddit - - - - - -telereddit_services_gfycat_service - -telereddit. -services. -gfycat_service - - - -requests->telereddit_services_gfycat_service - - - - - - -telereddit_services_imgur_service - -telereddit. -services. -imgur_service - - - -requests->telereddit_services_imgur_service - - - - - - -telereddit_services_service - -telereddit. -services. -service - - - -requests->telereddit_services_service - - - - - - - -telereddit_services_vreddit_service - -telereddit. -services. -vreddit_service - - - -requests->telereddit_services_vreddit_service - - - - - -sentry_sdk - -sentry_sdk - - - -telereddit_exceptions - -telereddit. -exceptions - - - -sentry_sdk->telereddit_exceptions - - - - -telereddit_telereddit - -telereddit. -telereddit - - - -sentry_sdk->telereddit_telereddit - - - - - -telegram - -telegram - - - -telereddit_config_config - -telereddit. -config. -config - - - -telegram->telereddit_config_config - - - - - - -telereddit_linker - -telereddit. -linker - - - -telegram->telereddit_linker - - - - - -telegram->telereddit_telereddit - - - - -telegram_ext - -telegram.ext - - - -telegram_ext->telereddit_telereddit - - - - -telereddit___main__ - -telereddit. -__main__ - - - -telereddit_config - -telereddit. -config - - - -telereddit_config->telereddit_exceptions - - - - - -telereddit_config->telereddit_helpers - - - - - -telereddit_config->telereddit_linker - - - - - - -telereddit_config->telereddit_reddit - - - - -telereddit_config->telereddit_services_gfycat_service - - - - - - -telereddit_config->telereddit_services_imgur_service - - - - - -telereddit_config->telereddit_telereddit - - - - - - -telereddit_config_config->telereddit_exceptions - - - - - - -telereddit_config_config->telereddit_helpers - - - - - -telereddit_config_config->telereddit_linker - - - - - -telereddit_config_config->telereddit_reddit - - - - - - -telereddit_config_config->telereddit_services_gfycat_service - - - - -telereddit_config_config->telereddit_services_imgur_service - - - - - - - -telereddit_config_config->telereddit_telereddit - - - - - -telereddit_config_secret - -telereddit. -config. -secret - - - -telereddit_config_secret_dev - -telereddit. -config. -secret_dev - - - -telereddit_config_secret->telereddit_config_secret_dev - - - - - -telereddit_config_secret_generic - -telereddit. -config. -secret_generic - - - -telereddit_config_secret->telereddit_config_secret_generic - - - - - -telereddit_config_secret_github - -telereddit. -config. -secret_github - - - -telereddit_config_secret->telereddit_config_secret_github - - - - - -telereddit_config_secret_prod - -telereddit. -config. -secret_prod - - - -telereddit_config_secret->telereddit_config_secret_prod - - - - - -telereddit_config_secret_dev->telereddit_config_secret_github - - - - - -telereddit_config_secret_dev->telereddit_config_secret_prod - - - - - -telereddit_exceptions->telereddit_linker - - - - - - -telereddit_exceptions->telereddit_reddit - - - - - - -telereddit_exceptions->telereddit_services_gfycat_service - - - - - - - -telereddit_exceptions->telereddit_services_service - - - - -telereddit_exceptions->telereddit_tests_test_reddit - - - - - -telereddit_exceptions->telereddit_tests_test_services - - - - - - -telereddit_helpers->telereddit_linker - - - - - -telereddit_helpers->telereddit_reddit - - - - - - -telereddit_helpers->telereddit_services_vreddit_service - - - - - -telereddit_services_youtube_service - -telereddit. -services. -youtube_service - - - -telereddit_helpers->telereddit_services_youtube_service - - - - - -telereddit_helpers->telereddit_telereddit - - - - - -telereddit_helpers->telereddit_tests_test_helpers - - - - - -telereddit_linker->telereddit_telereddit - - - - - -telereddit_tests_test_linker - -telereddit. -tests. -test_linker - - - -telereddit_linker->telereddit_tests_test_linker - - - - - -telereddit_models - -telereddit. -models - - - -telereddit_models->telereddit_linker - - - - - -telereddit_models->telereddit_reddit - - - - - - -telereddit_services_generic_service - -telereddit. -services. -generic_service - - - -telereddit_models->telereddit_services_generic_service - - - - - -telereddit_models->telereddit_services_gfycat_service - - - - - -telereddit_models->telereddit_services_imgur_service - - - - - - -telereddit_models->telereddit_services_vreddit_service - - - - - -telereddit_models->telereddit_services_youtube_service - - - - - - -telereddit_models->telereddit_tests_test_post - - - - -telereddit_models->telereddit_tests_test_reddit - - - - -telereddit_models->telereddit_tests_test_services - - - - -telereddit_models_content_type - -telereddit. -models. -content_type - - - -telereddit_models_media - -telereddit. -models. -media - - - -telereddit_models_content_type->telereddit_models_media - - - - - -telereddit_models_post - -telereddit. -models. -post - - - -telereddit_models_content_type->telereddit_models_post - - - - - -telereddit_models_content_type->telereddit_reddit - - - - - -telereddit_models_content_type->telereddit_services_generic_service - - - - - -telereddit_models_content_type->telereddit_services_gfycat_service - - - - - -telereddit_models_content_type->telereddit_services_imgur_service - - - - - -telereddit_models_content_type->telereddit_services_vreddit_service - - - - - -telereddit_models_content_type->telereddit_services_youtube_service - - - - - - -telereddit_models_content_type->telereddit_tests_test_post - - - - -telereddit_models_content_type->telereddit_tests_test_reddit - - - - -telereddit_models_content_type->telereddit_tests_test_services - - - - -telereddit_models_media->telereddit_linker - - - - - -telereddit_models_media->telereddit_services_generic_service - - - - - -telereddit_models_media->telereddit_services_gfycat_service - - - - - -telereddit_models_media->telereddit_services_imgur_service - - - - - -telereddit_models_media->telereddit_services_vreddit_service - - - - - - -telereddit_models_media->telereddit_services_youtube_service - - - - - -telereddit_models_media->telereddit_tests_test_post - - - - - - -telereddit_models_media->telereddit_tests_test_reddit - - - - - -telereddit_models_post->telereddit_reddit - - - - - -telereddit_models_post->telereddit_tests_test_post - - - - - - -telereddit_models_post->telereddit_tests_test_reddit - - - - - - - - -telereddit_reddit->telereddit_linker - - - - - -telereddit_reddit->telereddit_tests_test_reddit - - - - -telereddit_services - -telereddit. -services - - - -telereddit_services->telereddit_reddit - - - - - -telereddit_services->telereddit_tests_test_services - - - - - - -telereddit_services_services_wrapper - -telereddit. -services. -services_wrapper - - - -telereddit_services_generic_service->telereddit_services_services_wrapper - - - - - -telereddit_services_gfycat_service->telereddit_services_services_wrapper - - - - - -telereddit_services_gfycat_service->telereddit_tests_test_services - - - - - - -telereddit_services_imgur_service->telereddit_services_services_wrapper - - - - - -telereddit_services_service->telereddit_services_generic_service - - - - - -telereddit_services_service->telereddit_services_gfycat_service - - - - - -telereddit_services_service->telereddit_services_imgur_service - - - - - -telereddit_services_service->telereddit_services_vreddit_service - - - - - -telereddit_services_service->telereddit_services_youtube_service - - - - - -telereddit_services_services_wrapper->telereddit_reddit - - - - -telereddit_services_services_wrapper->telereddit_tests_test_services - - - - -telereddit_services_vreddit_service->telereddit_services_services_wrapper - - - - - -telereddit_services_youtube_service->telereddit_services_services_wrapper - - - - - -telereddit_telereddit->telereddit___main__ - - - - - diff --git a/docs/images/packages_telereddit.png b/docs/images/packages_telereddit.png new file mode 100644 index 0000000..5e21cad Binary files /dev/null and b/docs/images/packages_telereddit.png differ diff --git a/docs/telereddit/exceptions.html b/docs/telereddit/exceptions.html index 34c1e80..07e764e 100644 --- a/docs/telereddit/exceptions.html +++ b/docs/telereddit/exceptions.html @@ -37,7 +37,7 @@

Classes

class TeleredditError -(msg, data=None, capture=False) +(msg: Any, data: Any = None, capture: bool = False)

Base class for all catched exceptions.

@@ -75,7 +75,7 @@

Subclasses

class AuthenticationError -(data=None, capture=True) +(data: Any = None, capture: bool = True)

Raised when a service cannot authenticate to the API provider.

@@ -88,7 +88,7 @@

Ancestors

class SubredditError -(msg, data=None, capture=False) +(msg: Any, data: Any = None, capture: bool = False)

Base class for subreddit related exceptions.

@@ -110,7 +110,7 @@

Subclasses

class PostError -(msg, data=None, capture=True) +(msg: Any, data: Any = None, capture: bool = True)

Base class for post related exceptions.

@@ -133,7 +133,7 @@

Subclasses

class MediaError -(msg, data=None, capture=True) +(msg: Any, data: Any = None, capture: bool = True)

Base class for media related exceptions.

@@ -155,7 +155,7 @@

Subclasses

class SubredditPrivateError -(data=None, capture=False) +(data: Any = None, capture: bool = False)

Raised when the subreddit is private, and therefore cannot be fetched.

@@ -169,7 +169,7 @@

Ancestors

class SubredditDoesntExistError -(data=None, capture=False) +(data: Any = None, capture: bool = False)

Raised when the subreddit does not exist.

@@ -183,7 +183,7 @@

Ancestors

class PostRequestError -(data=None, capture=True) +(data: Any = None, capture: bool = True)

Raised when there's an error in the post request.

@@ -200,7 +200,7 @@

Ancestors

class PostRetrievalError -(data=None, capture=True) +(data: Any = None, capture: bool = True)

Raised when there's an error in the post json.

@@ -216,7 +216,7 @@

Ancestors

class PostSendError -(data=None, capture=True) +(data: Any = None, capture: bool = True)

Raised when there's an error in sending the post to the Telegram chat.

@@ -232,14 +232,14 @@

Ancestors

class PostEqualsMessageError -(data=None, capture=False) +(data: Any = None, capture: bool = False)

Raised when the post in the Telegram message is the same as the retrieved.

Capture

This error is useful when editing a Telegram message with a different post. -It is thus raised as a correct program flow, and therefore it should not be -captured from Sentry.

+It is thus raised as a correct program flow, and therefore it should not +be captured from Sentry.

Ancestors

class MediaTooBigError -(data=None, capture=True) +(data: Any = None, capture: bool = True)

Raised when post media exceeds the max media size allowed by Telegram APIs.

@@ -269,7 +269,7 @@

Ancestors

class MediaRetrievalError -(data=None, capture=True) +(data: Any = None, capture: bool = True)

Raised when there's an error in the media retrieval request.

diff --git a/docs/telereddit/helpers.html b/docs/telereddit/helpers.html index 75fcdab..4c25c29 100644 --- a/docs/telereddit/helpers.html +++ b/docs/telereddit/helpers.html @@ -32,7 +32,7 @@

Module telereddit.helpers

Functions

-def get_random_post_url(subreddit) +def get_random_post_url(subreddit: str) ‑> str

Return the "random post" url relative to the Reddit API.

@@ -48,7 +48,7 @@

Returns

-def get_subreddit_names(text) +def get_subreddit_names(text: str) ‑> List[str]

Return a list of the ("r/" prefixed) subreddit names present in the text.

@@ -70,7 +70,7 @@

Returns

-def get_subreddit_name(text, reverse=False) +def get_subreddit_name(text: str, reverse: bool = False) ‑> Union[str, NoneType]

Return the first (or last) ("r/" prefixed) subreddit name in the given text.

@@ -91,7 +91,7 @@

Returns

-def escape_markdown(text) +def escape_markdown(text: str) ‑> str

Return the given text with escaped common markdown characters.

@@ -112,7 +112,7 @@

Returns

-def truncate_text(text, length=200) +def truncate_text(text: str, length: int = 200) ‑> str

Return the given text, truncated at length characters, plus ellipsis.

@@ -134,7 +134,7 @@

Returns

-def polish_text(text) +def polish_text(text: str) ‑> str

Return the given text without newline characters.

@@ -150,7 +150,7 @@

Returns

-def get_urls_from_text(text) +def get_urls_from_text(text: str) ‑> List[str]

Return a list of the reddit urls present in the given text.

@@ -166,7 +166,7 @@

Returns

-def get(obj, attr, default=None) +def get(obj: Any, attr: str, default: Any = None) ‑> Any

Return the value of attr if it exists and is not None, default otherwise.

@@ -192,7 +192,7 @@

Returns

-def chained_get(obj, attrs, default=None) +def chained_get(obj: object, attrs: List[str], default: Any = None) ‑> Any

Get for nested objects.

diff --git a/docs/telereddit/index.html b/docs/telereddit/index.html index 762445c..2839f75 100644 --- a/docs/telereddit/index.html +++ b/docs/telereddit/index.html @@ -71,6 +71,13 @@

Installation

Bugs and feature requests

If you want to report a bug or would like a feature to be added, feel free to open an issue.

+

Versioning

+

We follow Semantic Versioning. The version X.Y.Z indicates:

+

License

GPL v3 - Copyright 2020 © fabio.sangregorio.dev.

@@ -137,6 +144,7 @@

Index

  • Installation
  • Bugs and feature requests
  • +
  • Versioning
  • License
  • diff --git a/docs/telereddit/linker.html b/docs/telereddit/linker.html index 93cd254..0fc9954 100644 --- a/docs/telereddit/linker.html +++ b/docs/telereddit/linker.html @@ -35,7 +35,7 @@

    Classes

    class Linker -(chat_id) +(chat_id: int)

    Handle a single telereddit request.

    @@ -52,15 +52,67 @@

    Attributes

    Class variables

    -
    var bot
    +
    var bot : telegram.bot.Bot
    -
    +

    Create a new Mock object. Mock takes several optional arguments +that specify the behaviour of the Mock object:

    +
      +
    • spec: This can be either a list of strings or an existing object (a +class or instance) that acts as the specification for the mock object. If +you pass in an object then a list of strings is formed by calling dir on +the object (excluding unsupported magic attributes and methods). Accessing +any attribute not in this list will raise an AttributeError.
    • +
    +

    If spec is an object (rather than a list of strings) then +mock.__class__ returns the class of the spec object. This allows mocks +to pass isinstance tests.

    +
      +
    • +

      spec_set: A stricter variant of spec. If used, attempting to set +or get an attribute on the mock that isn't on the object passed as +spec_set will raise an AttributeError.

      +
    • +
    • +

      side_effect: A function to be called whenever the Mock is called. See +the side_effect attribute. Useful for raising exceptions or +dynamically changing return values. The function is called with the same +arguments as the mock, and unless it returns DEFAULT, the return +value of this function is used as the return value.

      +
    • +
    +

    If side_effect is an iterable then each call to the mock will return +the next value from the iterable. If any of the members of the iterable +are exceptions they will be raised instead of returned.

    +
      +
    • +

      return_value: The value returned when the mock is called. By default +this is a new Mock (created on first access). See the +return_value attribute.

      +
    • +
    • +

      wraps: Item for the mock object to wrap. If wraps is not None then +calling the Mock will pass the call through to the wrapped object +(returning the real result). Attribute access on the mock will return a +Mock object that wraps the corresponding attribute of the wrapped object +(so attempting to access an attribute that doesn't exist will raise an +AttributeError).

      +
    • +
    +

    If the mock has an explicit return_value set then calls are not passed +to the wrapped object and the return_value is returned instead.

    +
      +
    • name: If the mock has a name then it will be used in the repr of the +mock. This can be useful for debugging. The name is propagated to child +mocks.
    • +
    +

    Mocks can also be called with arbitrary keyword arguments. These will be +used to set attributes on the mock after it is created.

    Static methods

    -def set_bot(bot) +def set_bot(bot: telegram.bot.Bot) ‑> NoneType

    Set the python-telegram-bot's Bot instance for the Linker object.

    @@ -78,7 +130,7 @@

    Parameters

    Methods

    -def get_args(self, override_dict={}) +def get_args(self, override_dict: Union[dict, NoneType] = None) ‑> dict

    Get the args parameters potentially overriding some of them.

    @@ -95,7 +147,7 @@

    Returns

    args

    -def send_random_post(self, subreddit) +def send_random_post(self, subreddit: str) ‑> NoneType

    Send a random post to the chat from the given subreddit.

    @@ -112,7 +164,7 @@

    Parameters

    -def send_post_from_url(self, post_url) +def send_post_from_url(self, post_url: str) ‑> NoneType

    Try to send the reddit post relative to post_url to the chat.

    @@ -124,7 +176,7 @@

    Parameters

    -def send_post(self, post_url, from_url=False) +def send_post(self, post_url: str, from_url: bool = False) ‑> NoneType

    Send the reddit post relative to post_url to the chat.

    @@ -142,7 +194,7 @@

    Parameters

    -def edit_result(self, message) +def edit_result(self, message: telegram.message.Message) ‑> NoneType

    Edit the given message with a new post from that subreddit.

    @@ -155,7 +207,7 @@

    Parameters

    -def edit_random_post(self, message, subreddit) +def edit_random_post(self, message: telegram.message.Message, subreddit: str) ‑> NoneType

    Edit the current Telegram message with another random Reddit post.

    diff --git a/docs/telereddit/models/media.html b/docs/telereddit/models/media.html index d19b421..2af59f4 100644 --- a/docs/telereddit/models/media.html +++ b/docs/telereddit/models/media.html @@ -35,7 +35,7 @@

    Classes

    class Media -(url, media_type: ContentType, size=None) +(url: str, media_type: ContentType, size: Union[int, NoneType] = None)

    Represents a media content in the application.

    diff --git a/docs/telereddit/models/post.html b/docs/telereddit/models/post.html index 0fa831a..7f9fc99 100644 --- a/docs/telereddit/models/post.html +++ b/docs/telereddit/models/post.html @@ -35,7 +35,7 @@

    Classes

    class Post -(subreddit, permalink, title, text, media=None) +(subreddit: str, permalink: str, title: str, text: str, media: Union[Media, NoneType] = None)

    Represents a Reddit post.

    @@ -59,7 +59,7 @@

    Parameters

    Methods

    -def get_msg(self) +def get_msg(self) ‑> str

    Get the full message of the post.

    @@ -67,7 +67,7 @@

    Methods

    a link to the subreddit and a link to the post.

    -def get_type(self) +def get_type(self) ‑> ContentType

    Return the post type: this is determined by the media type, if present.

    diff --git a/docs/telereddit/reddit.html b/docs/telereddit/reddit.html index 750f330..ca1be3d 100644 --- a/docs/telereddit/reddit.html +++ b/docs/telereddit/reddit.html @@ -34,7 +34,7 @@

    Module telereddit.reddit

    Functions

    -def get_post(post_url) +def get_post(post_url: str) ‑> Post

    Get the post from the Reddit API and construct the Post object.

    diff --git a/docs/telereddit/services/generic_service.html b/docs/telereddit/services/generic_service.html index d839fef..8045ff2 100644 --- a/docs/telereddit/services/generic_service.html +++ b/docs/telereddit/services/generic_service.html @@ -41,11 +41,11 @@

    Classes

    Ancestors

    • Service
    • -
    • abc.ABC
    • +
    • icontract._metaclass.DBC

    Class variables

    -
    var has_external_request
    +
    var has_external_request : bool

    Inherited from: @@ -54,7 +54,7 @@

    Class variables

    True if the service needs to reach out to an external http endpoint, False otherwise.

    -
    var is_authenticated
    +
    var is_authenticated : bool

    Inherited from: @@ -63,7 +63,7 @@

    Class variables

    True if the external request needs to be authenticated (i.e. with an Authorization header), False otherwise …

    -
    var access_token
    +
    var access_token : Union[str, NoneType]

    Inherited from: @@ -76,13 +76,13 @@

    Class variables

    Static methods

    -def postprocess(response) +def postprocess(cls, response) ‑> Media

    Override of Service.postprocess() method.

    -def preprocess(url, json) +def preprocess(cls, url: str, data: Any) ‑> str

    @@ -92,7 +92,7 @@

    Static methods

    Preprocess the media URL coming from Reddit json …

    -def get(url) +def get(cls, url: str) ‑> Union[requests.models.Response, str]

    @@ -102,7 +102,7 @@

    Static methods

    Get the media information …

    -def authenticate() +def authenticate() ‑> NoneType

    @@ -112,7 +112,7 @@

    Static methods

    Authenticate the service on the service provider API …

    -def get_media(url, json) +def get_media(cls, url: str, data: Any) ‑> Media

    @@ -149,9 +149,9 @@

    get
  • authenticate
  • get_media
  • -
  • has_external_request
  • -
  • is_authenticated
  • -
  • access_token
  • +
  • has_external_request
  • +
  • is_authenticated
  • +
  • access_token
  • diff --git a/docs/telereddit/services/gfycat_service.html b/docs/telereddit/services/gfycat_service.html index 68bc06c..60cd426 100644 --- a/docs/telereddit/services/gfycat_service.html +++ b/docs/telereddit/services/gfycat_service.html @@ -43,11 +43,11 @@

    Notes

    Ancestors

    • Service
    • -
    • abc.ABC
    • +
    • icontract._metaclass.DBC

    Class variables

    -
    var is_authenticated
    +
    var is_authenticated : bool

    Inherited from: @@ -56,7 +56,7 @@

    Class variables

    True if the external request needs to be authenticated (i.e. with an Authorization header), False otherwise …

    -
    var access_token
    +
    var access_token : Union[str, NoneType]

    Inherited from: @@ -65,7 +65,7 @@

    Class variables

    Contains the access token for the OAuth authentication if present, None otherwise …

    -
    var has_external_request
    +
    var has_external_request : bool

    Inherited from: @@ -78,21 +78,21 @@

    Class variables

    Static methods

    -def preprocess(url, json) +def preprocess(cls, url: str, data: Any) ‑> str

    Override of Service.preprocess() method.

    Extracts the gfycat Id from the url and constructs the provider url.

    -def get(url) +def get(cls, url: str) ‑> requests.models.Response

    Override of Service.get() method.

    Makes a call to the provider's API.

    -def postprocess(response) +def postprocess(cls, response) ‑> Media

    Override of Service.postprocess() method.

    @@ -100,14 +100,14 @@

    Static methods

    present.

    -def authenticate() +def authenticate() ‑> NoneType

    Override of Service.authenticate() method.

    Authenticates the service through OAuth.

    -def get_media(url, json) +def get_media(cls, url: str, data: Any) ‑> Media

    diff --git a/docs/telereddit/services/imgur_service.html b/docs/telereddit/services/imgur_service.html index 7d219f7..d09f5ec 100644 --- a/docs/telereddit/services/imgur_service.html +++ b/docs/telereddit/services/imgur_service.html @@ -41,11 +41,11 @@

    Classes

    Ancestors

    • Service
    • -
    • abc.ABC
    • +
    • icontract._metaclass.DBC

    Class variables

    -
    var has_external_request
    +
    var has_external_request : bool

    Inherited from: @@ -54,7 +54,7 @@

    Class variables

    True if the service needs to reach out to an external http endpoint, False otherwise.

    -
    var is_authenticated
    +
    var is_authenticated : bool

    Inherited from: @@ -63,7 +63,7 @@

    Class variables

    True if the external request needs to be authenticated (i.e. with an Authorization header), False otherwise …

    -
    var access_token
    +
    var access_token : Union[str, NoneType]

    Inherited from: @@ -76,7 +76,7 @@

    Class variables

    Static methods

    -def preprocess(url, json) +def preprocess(cls, url: str, data: Any) ‑> str

    Override of Service.preprocess() method.

    @@ -84,21 +84,21 @@

    Static methods

    url.

    -def get(url) +def get(cls, url: str) ‑> requests.models.Response

    Override of Service.get() method.

    Makes an API call with the client ID as authorization.

    -def postprocess(response) +def postprocess(cls, response) ‑> Media

    Override of Service.postprocess() method.

    Creates the right media object based on the size of provider's media.

    -def authenticate() +def authenticate() ‑> NoneType

    @@ -108,7 +108,7 @@

    Static methods

    Authenticate the service on the service provider API …

    -def get_media(url, json) +def get_media(cls, url: str, data: Any) ‑> Media

    @@ -145,9 +145,9 @@

    postprocess
  • authenticate
  • get_media
  • -
  • has_external_request
  • -
  • is_authenticated
  • -
  • access_token
  • +
  • has_external_request
  • +
  • is_authenticated
  • +
  • access_token
  • diff --git a/docs/telereddit/services/service.html b/docs/telereddit/services/service.html index 003f162..b9ed048 100644 --- a/docs/telereddit/services/service.html +++ b/docs/telereddit/services/service.html @@ -58,7 +58,7 @@

    Notes

    for the first time on the Service creation.

    Ancestors

      -
    • abc.ABC
    • +
    • icontract._metaclass.DBC

    Subclasses

      @@ -70,12 +70,12 @@

      Subclasses

    Class variables

    -
    var has_external_request
    +
    var has_external_request : bool

    True if the service needs to reach out to an external http endpoint, False otherwise.

    -
    var is_authenticated
    +
    var is_authenticated : bool

    True if the external request needs to be authenticated (i.e. with an Authorization header), False otherwise.

    @@ -84,7 +84,7 @@

    Class variables

    This is taken into account only if has_external_request is set to True

    -
    var access_token
    +
    var access_token : Union[str, NoneType]

    Contains the access token for the OAuth authentication if present, None otherwise.

    @@ -97,7 +97,7 @@

    Class variables

    Static methods

    -def preprocess(url, json) +def preprocess(cls, url: str, data: Any) ‑> str

    Preprocess the media URL coming from Reddit json.

    @@ -107,7 +107,7 @@

    Parameters

    url : str
    Reddit media URL to preprocess.
    -
    json : json
    +
    data : json
    Json from the Reddit API which contains the post data. Used to get fallback media urls for specific services.
    @@ -118,7 +118,7 @@

    Returns

    -def get(url) +def get(cls, url: str) ‑> Union[requests.models.Response, str]

    Get the media information.

    @@ -139,7 +139,7 @@

    Returns

    Response from the service provider API.

    -def postprocess(response) +def postprocess(cls, response: Union[requests.models.Response, str]) ‑> Media

    From the service provider API response create the media object.

    @@ -159,7 +159,7 @@

    Returns

    Media object related to the media retrieval process.

    -def authenticate() +def authenticate() ‑> NoneType

    Authenticate the service on the service provider API.

    @@ -167,7 +167,7 @@

    Returns

    token.

    -def get_media(url, json) +def get_media(cls, url: str, data: Any) ‑> Media

    Entrypoint of the class.

    @@ -192,7 +192,7 @@

    Parameters

    url : str
    Media URL from the Reddit API json.
    -
    json : json
    +
    data : json
    Json from the Reddit API which contains the post data. Used to get fallback media urls for specific services.
    diff --git a/docs/telereddit/services/services_wrapper.html b/docs/telereddit/services/services_wrapper.html index 7fdc3df..a1707e5 100644 --- a/docs/telereddit/services/services_wrapper.html +++ b/docs/telereddit/services/services_wrapper.html @@ -43,23 +43,23 @@

    Classes

    An instance for each service class is set at class initialization.

    Class variables

    -
    var gfycat
    +
    var gfycatGfycat
    -
    var vreddit
    +
    var vredditVreddit
    -
    var imgur
    +
    var imgurImgur
    -
    var youtube
    +
    var youtubeYoutube
    -
    var generic
    +
    var genericGeneric
    @@ -67,7 +67,7 @@

    Class variables

    Static methods

    -def get_media(url, json={}) +def get_media(cls, url: str, data: Any = None) ‑> Media

    Given the url from the Reddit json, return the corresponding media obj.

    @@ -76,7 +76,7 @@

    Parameters

    url : str
    Url from Reddit API json.
    -
    json : json
    +
    data : json

    (Default value = {})

    Reddit data json containing media fallback urls.

    diff --git a/docs/telereddit/services/vreddit_service.html b/docs/telereddit/services/vreddit_service.html index 5ddd5a4..7a0692e 100644 --- a/docs/telereddit/services/vreddit_service.html +++ b/docs/telereddit/services/vreddit_service.html @@ -41,11 +41,11 @@

    Classes

    Ancestors

    • Service
    • -
    • abc.ABC
    • +
    • icontract._metaclass.DBC

    Class variables

    -
    var has_external_request
    +
    var has_external_request : bool

    Inherited from: @@ -54,7 +54,7 @@

    Class variables

    True if the service needs to reach out to an external http endpoint, False otherwise.

    -
    var is_authenticated
    +
    var is_authenticated : bool

    Inherited from: @@ -63,7 +63,7 @@

    Class variables

    True if the external request needs to be authenticated (i.e. with an Authorization header), False otherwise …

    -
    var access_token
    +
    var access_token : Union[str, NoneType]

    Inherited from: @@ -76,7 +76,7 @@

    Class variables

    Static methods

    -def preprocess(url, json) +def preprocess(cls, url: str, data: Any) ‑> str

    Override of Service.preprocess() method.

    @@ -86,14 +86,14 @@

    Static methods

    information in every specific case.

    -def postprocess(response) +def postprocess(cls, response) ‑> Media

    Override of Service.postprocess() method.

    Constructs the media object.

    -def get(url) +def get(cls, url: str) ‑> Union[requests.models.Response, str]

    @@ -103,7 +103,7 @@

    Static methods

    Get the media information …

    -def authenticate() +def authenticate() ‑> NoneType

    @@ -113,7 +113,7 @@

    Static methods

    Authenticate the service on the service provider API …

    -def get_media(url, json) +def get_media(cls, url: str, data: Any) ‑> Media

    @@ -150,9 +150,9 @@

    get
  • authenticate
  • get_media
  • -
  • has_external_request
  • -
  • is_authenticated
  • -
  • access_token
  • +
  • has_external_request
  • +
  • is_authenticated
  • +
  • access_token
  • diff --git a/docs/telereddit/services/youtube_service.html b/docs/telereddit/services/youtube_service.html index 034f02a..7efada7 100644 --- a/docs/telereddit/services/youtube_service.html +++ b/docs/telereddit/services/youtube_service.html @@ -44,11 +44,11 @@

    Notes

    Ancestors

    • Service
    • -
    • abc.ABC
    • +
    • icontract._metaclass.DBC

    Class variables

    -
    var access_token
    +
    var access_token : Union[str, NoneType]

    Inherited from: @@ -57,7 +57,7 @@

    Class variables

    Contains the access token for the OAuth authentication if present, None otherwise …

    -
    var is_authenticated
    +
    var is_authenticated : bool

    Inherited from: @@ -66,7 +66,7 @@

    Class variables

    True if the external request needs to be authenticated (i.e. with an Authorization header), False otherwise …

    -
    var has_external_request
    +
    var has_external_request : bool

    Inherited from: @@ -79,28 +79,28 @@

    Class variables

    Static methods

    -def preprocess(url, json) +def preprocess(cls, url: str, data: Any) ‑> str

    Override of Service.preprocess() method.

    Gets the youtube url from reddit json.

    -def get(url) +def get(cls, url: str) ‑> str

    Override of Service.get() method.

    Fake get: simply returns the url given as parameter.

    -def postprocess(url) +def postprocess(cls, response) ‑> Media

    Override of Service.postprocess() method.

    Constructs the media object.

    -def authenticate() +def authenticate() ‑> NoneType

    @@ -110,7 +110,7 @@

    Static methods

    Authenticate the service on the service provider API …

    -def get_media(url, json) +def get_media(cls, url: str, data: Any) ‑> Media

    diff --git a/docs/telereddit/telereddit.html b/docs/telereddit/telereddit.html index c5da28b..3a9d272 100644 --- a/docs/telereddit/telereddit.html +++ b/docs/telereddit/telereddit.html @@ -34,7 +34,7 @@

    Module telereddit.telereddit

    Functions

    -def on_chat_message(update: telegram.update.Update, context: telegram.ext.callbackcontext.CallbackContext) +def on_chat_message(update: telegram.update.Update, context: telegram.ext.callbackcontext.CallbackContext) ‑> NoneType

    Entrypoint of the bot's logic. Handles a single update message.

    @@ -47,7 +47,7 @@

    Parameters

    -def on_callback_query(update: telegram.update.Update, context: telegram.ext.callbackcontext.CallbackContext) +def on_callback_query(update: telegram.update.Update, context: telegram.ext.callbackcontext.CallbackContext) ‑> NoneType

    Handle all the several types of callback queries.

    @@ -61,7 +61,7 @@

    Parameters

    -def main() +def main() ‑> NoneType

    Entrypoint of telereddit. Handles configuration, setup and start of the bot.

    diff --git a/docs/telereddit/tests/test_helpers.html b/docs/telereddit/tests/test_helpers.html index 09b6dba..24e826d 100644 --- a/docs/telereddit/tests/test_helpers.html +++ b/docs/telereddit/tests/test_helpers.html @@ -104,6 +104,12 @@

    Methods

    +
    +def test_get_random_post_url_invalid(self) +
    +
    +
    +
    def test_get_subreddit_names_valid_0(*a)
    @@ -146,8 +152,8 @@

    Methods

    -
    -def test_get_subreddit_name_4(*a) +
    +def test_get_subreddit_name_invalid(self)
    @@ -170,14 +176,8 @@

    Methods

    -
    -def test_truncate_text_3(*a) -
    -
    -
    -
    -
    -def test_truncate_text_4(*a) +
    +def test_truncate_invalid(self)
    @@ -254,6 +254,7 @@

    Index

    TestHelpers

    Methods

    +
    +def test_bot_not_none(self) +
    +
    +
    +
    def test_get_args(self)
    @@ -90,6 +104,156 @@

    Methods

    +
    +def test_send_random_post(self, mock_err_function, mock_send_post) +
    +
    +
    +
    +
    +def test_send_random_post_invalid(self, mock_err_function, mock_send_post) +
    +
    +
    +
    +
    +def test_send_post_from_url(self, mock_err_function, mock_send_post) +
    +
    +
    +
    +
    +def test_send_post_from_url_invalid(self, mock_err_function, mock_send_post) +
    +
    +
    +
    +
    +def test_send_post_0(*a) +
    +
    +
    +
    +
    +def test_send_post_1(*a) +
    +
    +
    +
    +
    +def test_send_post_2(*a) +
    +
    +
    +
    +
    +def test_send_post_3(*a) +
    +
    +
    +
    +
    +def test_send_post_4(*a) +
    +
    +
    +
    +
    +def test_send_post_no_type(self, mock_get_post) +
    +
    +
    +
    +
    +def test_send_post_from_url_true(self, mock_get_post) +
    +
    +
    +
    +
    +def test_send_post_invalid(self, mock_get_post) +
    +
    +
    +
    +
    +def test_send_post_media_too_big(self, mock_get_post) +
    +
    +
    +
    +
    +def test_send_post_err(self, mock_get_post, mock_send_message) +
    +
    +
    +
    +
    +def test_edit_result_none(self) +
    +
    +
    +
    +
    +def test_edit_result(self, mock_edit_random_post) +
    +
    +
    +
    +
    +def test_edit_result_invalid(self, mock_edit_random_post) +
    +
    +
    +
    +
    +def test_edit_random_post_text(self, mock_get_post) +
    +
    +
    +
    +
    +def test_edit_random_post_invalid(self, mock_get_post) +
    +
    +
    +
    +
    +def test_edit_random_post_youtube(self, mock_get_post) +
    +
    +
    +
    +
    +def test_edit_random_post_types_0(*a) +
    +
    +
    +
    +
    +def test_edit_random_post_types_1(*a) +
    +
    +
    +
    +
    +def test_edit_random_post_types_2(*a) +
    +
    +
    +
    +
    +def test_send_exception_message(self, mock_send_message) +
    +
    +
    +
    +
    +def test_send_exception_message_no_kb(self, mock_send_message) +
    +
    +
    +
    @@ -113,9 +277,37 @@

    Index

  • TestLinker

  • diff --git a/docs/telereddit/tests/test_reddit.html b/docs/telereddit/tests/test_reddit.html index b37bf7e..d22b3a2 100644 --- a/docs/telereddit/tests/test_reddit.html +++ b/docs/telereddit/tests/test_reddit.html @@ -106,6 +106,12 @@

    Methods

    +
    +def test_get_json(self, mock_get) +
    +
    +
    +
    def test_get_post_0(*a)
    @@ -163,6 +169,7 @@

    test_get_json_404
  • test_get_json_private
  • test_get_json_valid
  • +
  • test_get_json
  • test_get_post_0
  • test_get_post_1
  • test_get_post_2
  • diff --git a/docs/telereddit/tests/test_services.html b/docs/telereddit/tests/test_services.html index bfaecae..85a95de 100644 --- a/docs/telereddit/tests/test_services.html +++ b/docs/telereddit/tests/test_services.html @@ -160,6 +160,12 @@

    Methods

    +
    +def test_generic_video(self, mock_get) +
    +
    +
    +
    def test_gfycat_authentication_fail(self, mock_post)
    @@ -206,6 +212,7 @@

    test_vreddit_1
  • test_generic_0
  • test_generic_1
  • +
  • test_generic_video
  • test_gfycat_authentication_fail
  • test_gfycat_post_fail
  • test_youtube
  • diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..e8d6d8e --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,4 @@ +-r requirements.txt +-r docs.txt +-r test.txt +-r lint.txt \ No newline at end of file diff --git a/requirements/docs.txt b/requirements/docs.txt new file mode 100644 index 0000000..ede9b35 --- /dev/null +++ b/requirements/docs.txt @@ -0,0 +1,4 @@ +pdoc3>=0.8.1 +pydocstyle>=5.0.2 +docstr-coverage>=1.0.5 +pylint>=2.6.0 \ No newline at end of file diff --git a/requirements/lint.txt b/requirements/lint.txt new file mode 100644 index 0000000..283798b --- /dev/null +++ b/requirements/lint.txt @@ -0,0 +1,6 @@ +black>=19.10b0 +flake8>=3.7.9 +mypy>=0.782 +pylint>=2.6.0 +pydocstyle>=5.0.2 +pyicontract-lint>=2.1.0 \ No newline at end of file diff --git a/requirements.txt b/requirements/requirements.txt similarity index 53% rename from requirements.txt rename to requirements/requirements.txt index f778471..c120fa6 100644 Binary files a/requirements.txt and b/requirements/requirements.txt differ diff --git a/requirements/test.txt b/requirements/test.txt new file mode 100644 index 0000000..7a9518a --- /dev/null +++ b/requirements/test.txt @@ -0,0 +1,3 @@ +coverage>=5.0.4 +coveralls>=2.0.0 +parameterized>=0.7.4 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index f636b8e..42b444f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,10 +11,19 @@ match = (?!test_).*\.py match_dir = (?!test).* [pydeps] -noshow = True cluster = True max_bacon = 2 max_cluster_size = 3 exclude_exact = telereddit.config telereddit.services rmprefix = - telereddit. \ No newline at end of file + telereddit. + +[mypy] +ignore_missing_imports = True +[mypy-telereddit.tests.*] +ignore_errors = True + +[pep8] +max-line-length = 88 +ignore = E203,W503 +exclude = secret_*.py,tests diff --git a/telereddit/__main__.py b/telereddit/__main__.py index ad12fbd..529d7ef 100644 --- a/telereddit/__main__.py +++ b/telereddit/__main__.py @@ -1,4 +1,8 @@ -"""Main entrypoint of the application. Simply calls the main function of `telereddit`.""" +""" +Main entrypoint of the application. + +Simply calls the main function of `telereddit`. +""" import telereddit.telereddit as telereddit diff --git a/telereddit/config/config.py b/telereddit/config/config.py index ccf9412..214ed78 100644 --- a/telereddit/config/config.py +++ b/telereddit/config/config.py @@ -6,10 +6,12 @@ for a leaner one. """ -from telegram import InlineKeyboardMarkup, InlineKeyboardButton import os import importlib import logging +from typing import Any + +from telegram import InlineKeyboardMarkup, InlineKeyboardButton # type: ignore _delete_btn = InlineKeyboardButton(text="✕", callback_data="delete") @@ -18,20 +20,21 @@ _more_btn = InlineKeyboardButton(text="+", callback_data="more") # Dynamic environment secret configuration +secret: Any = None _env_key = os.environ.get("TELEREDDIT_MACHINE") if _env_key is not None: ENV = _env_key.lower() secret = importlib.import_module( f"telereddit.config.secret_{_env_key.lower()}" - ).secret_config + ).secret_config # type: ignore else: - logging.warn( + logging.warning( 'No "TELEREDDIT_MACHINE" environment variable found. Using generic secret.' ) ENV = "generic" secret = importlib.import_module( "telereddit.config.secret_generic" - ).secret_config + ).secret_config # type: ignore REDDIT_DOMAINS = ["reddit.com", "redd.it", "reddit.app.link"] MAX_POST_LENGTH = 500 diff --git a/telereddit/exceptions.py b/telereddit/exceptions.py index 0d51185..3807fbe 100644 --- a/telereddit/exceptions.py +++ b/telereddit/exceptions.py @@ -5,8 +5,10 @@ flow between two functions. """ -import sentry_sdk as sentry import traceback +from typing import Any +import logging +import sentry_sdk as sentry import telereddit.config.config as config @@ -37,7 +39,7 @@ class TeleredditError(Exception): """ - def __init__(self, msg, data=None, capture=False): + def __init__(self, msg: Any, data: Any = None, capture: bool = False): super().__init__(msg) if config.SENTRY_ENABLED: if data is not None: @@ -47,14 +49,18 @@ def __init__(self, msg, data=None, capture=False): if capture: sentry.capture_exception() traceback.print_exc() - print("\nException:", self.__class__.__name__) - print("Message: ", self, ", Data: ", data) + logging.exception( + "\nEXCEPTION: %s, MESSAGE: %s, DATA: %s", + self.__class__.__name__, + self, + data, + ) class AuthenticationError(TeleredditError): """Raised when a service cannot authenticate to the API provider.""" - def __init__(self, data=None, capture=True): + def __init__(self, data: Any = None, capture: bool = True): super().__init__("Authentication failed", data, capture) @@ -69,9 +75,6 @@ class SubredditError(TeleredditError): or not existing, and therfore **should not** be captured by Sentry. """ - def __init__(self, msg, data=None, capture=False): - super().__init__(msg, data, capture) - class PostError(TeleredditError): """ @@ -83,7 +86,7 @@ class PostError(TeleredditError): **should** be captured by Sentry. """ - def __init__(self, msg, data=None, capture=True): + def __init__(self, msg: Any, data: Any = None, capture: bool = True): super().__init__(msg, data, capture) @@ -98,21 +101,21 @@ class MediaError(TeleredditError): For this, unless specified otherwise, they **should** be captured by Sentry. """ - def __init__(self, msg, data=None, capture=True): + def __init__(self, msg: Any, data: Any = None, capture: bool = True): super().__init__(msg, data, capture) class SubredditPrivateError(SubredditError): """Raised when the subreddit is private, and therefore cannot be fetched.""" - def __init__(self, data=None, capture=False): + def __init__(self, data: Any = None, capture: bool = False): super().__init__("This subreddit is private.", data, capture) class SubredditDoesntExistError(SubredditError): """Raised when the subreddit does not exist.""" - def __init__(self, data=None, capture=False): + def __init__(self, data: Any = None, capture: bool = False): super().__init__("This subreddit doesn't exist.", data, capture) @@ -123,7 +126,7 @@ class PostRequestError(PostError): .. note:: Not to be confused with `PostRetrievalError` """ - def __init__(self, data=None, capture=True): + def __init__(self, data: Any = None, capture: bool = True): super().__init__("I can't find that subreddit.", data, capture) @@ -135,7 +138,7 @@ class PostRetrievalError(PostError): expected. """ - def __init__(self, data=None, capture=True): + def __init__(self, data: Any = None, capture: bool = True): super().__init__("The retrieval of the post failed.", data, capture) @@ -147,7 +150,7 @@ class PostSendError(PostError): APIs expect. """ - def __init__(self, data=None, capture=True): + def __init__(self, data: Any = None, capture: bool = True): super().__init__( "There has been an error in sending the post.", data, capture ) @@ -160,11 +163,11 @@ class PostEqualsMessageError(PostError): Capture ------- This error is useful when editing a Telegram message with a different post. - It is thus raised as a correct program flow, and therefore it **should not** be - captured from Sentry. + It is thus raised as a correct program flow, and therefore it **should not** + be captured from Sentry. """ - def __init__(self, data=None, capture=False): + def __init__(self, data: Any = None, capture: bool = False): super().__init__( "The retrieved post is equal to the already sent message.", data, @@ -181,12 +184,12 @@ class MediaTooBigError(MediaError): https://core.telegram.org/bots/api#sending-files """ - def __init__(self, data=None, capture=True): + def __init__(self, data: Any = None, capture: bool = True): super().__init__("Media is too big to be sent.", data, capture) class MediaRetrievalError(MediaError): """Raised when there's an error in the media retrieval request.""" - def __init__(self, data=None, capture=True): + def __init__(self, data: Any = None, capture: bool = True): super().__init__("Error in getting the media", data, capture) diff --git a/telereddit/helpers.py b/telereddit/helpers.py index 5bc3afa..e5d02c6 100644 --- a/telereddit/helpers.py +++ b/telereddit/helpers.py @@ -1,13 +1,22 @@ """Miscellaneous helpers for the whole application.""" +from typing import List, Optional, Any import re import requests +from requests import Response +from requests.exceptions import RequestException +import icontract from telereddit.config.config import MAX_TITLE_LENGTH import telegram -def get_random_post_url(subreddit): +@icontract.require( + lambda subreddit: subreddit is not None and len(subreddit) > 0, + "subreddit must not be None", +) +@icontract.ensure(lambda result, subreddit: subreddit in result) +def get_random_post_url(subreddit: str) -> str: """ Return the "random post" url relative to the Reddit API. @@ -25,7 +34,11 @@ def get_random_post_url(subreddit): return f"https://www.reddit.com/{subreddit}/random" -def get_subreddit_names(text): +@icontract.require( + lambda text: text is not None and len(text) > 0, + "text must not be None", +) +def get_subreddit_names(text: str) -> List[str]: """ Return a list of the ("r/" prefixed) subreddit names present in the text. @@ -50,7 +63,11 @@ def get_subreddit_names(text): return re.findall(regex, text, re.MULTILINE) -def get_subreddit_name(text, reverse=False): +@icontract.require( + lambda text, reverse: text is not None and len(text) > 0, + "text must not be None", +) +def get_subreddit_name(text: str, reverse: bool = False) -> Optional[str]: """ Return the first (or last) ("r/" prefixed) subreddit name in the given text. @@ -71,13 +88,16 @@ def get_subreddit_name(text, reverse=False): """ subs = get_subreddit_names(text) - if len(subs): + if len(subs) > 0: return subs[-1] if reverse else subs[0] - else: - return None + return None -def escape_markdown(text): +@icontract.require( + lambda text: text is not None, + "text must not be None", +) +def escape_markdown(text: str) -> str: """ Return the given text with escaped common markdown characters. @@ -99,7 +119,15 @@ def escape_markdown(text): return telegram.utils.helpers.escape_markdown(text, version=2) -def truncate_text(text, length=MAX_TITLE_LENGTH): +@icontract.require( + lambda text, length: text is not None, + "text must not be None", +) +@icontract.require( + lambda text, length: length > 0, + "length must not be <= 0", +) +def truncate_text(text: str, length: int = MAX_TITLE_LENGTH) -> str: """ Return the given text, truncated at `length` characters, plus ellipsis. @@ -120,12 +148,14 @@ def truncate_text(text, length=MAX_TITLE_LENGTH): New string containing the truncated text, plus ellipsis. """ - if length < 0: - return text return text[:length] + (text[length:] and "...") -def polish_text(text): +@icontract.require( + lambda text: text is not None and len(text) > 0, + "text must not be None", +) +def polish_text(text: str) -> str: """ Return the given text without newline characters. @@ -143,7 +173,11 @@ def polish_text(text): return text.replace("\n", " ") -def get_urls_from_text(text): +@icontract.require( + lambda text: text is not None and len(text) > 0, + "text must not be None", +) +def get_urls_from_text(text: str) -> List[str]: """ Return a list of the reddit urls present in the given text. @@ -160,31 +194,37 @@ def get_urls_from_text(text): """ polished = polish_text(text) urls = list() - for w in polished.split(" "): - w_lower = w.lower() + for word in polished.split(" "): + w_lower = word.lower() if "reddit.com" in w_lower: - urls.append(w.partition("/?")[0]) + urls.append(word.partition("/?")[0]) if "redd.it" in w_lower: urls.append( - f'https://www.reddit.com/comments/{w.partition("redd.it/")[2]}' + f'https://www.reddit.com/comments/{word.partition("redd.it/")[2]}' ) if "reddit.app.link" in w_lower: try: - r = requests.get( - w, + resp: Response = requests.get( + word, headers={"User-agent": "telereddit_bot"}, allow_redirects=False, ) - start = r.text.find("https://") - url = r.text[start : r.text.find('"', start)] + start = resp.text.find("https://") + url = resp.text[start : resp.text.find('"', start)] if len(url) > 0: urls.append(url.partition("/?")[0]) - except Exception: + except RequestException: pass return urls -def get(obj, attr, default=None): +@icontract.require( + lambda obj, attr, default: obj is not None, "obj must not be None" +) +@icontract.require( + lambda obj, attr, default: attr is not None, "attr must not be None" +) +def get(obj: Any, attr: str, default: Any = None) -> Any: """ Return the value of `attr` if it exists and is not None, default otherwise. @@ -212,7 +252,14 @@ def get(obj, attr, default=None): return obj[attr] if attr in obj and obj[attr] is not None else default -def chained_get(obj, attrs, default=None): +@icontract.require( + lambda obj, attrs, default: obj is not None, "obj must not be None" +) +@icontract.require( + lambda obj, attrs, default: attrs is not None and len(attrs) > 0, + "attrs must not be None", +) +def chained_get(obj: object, attrs: List[str], default: Any = None) -> Any: """ Get for nested objects. diff --git a/telereddit/linker.py b/telereddit/linker.py index 7b6cb92..b0f19e5 100644 --- a/telereddit/linker.py +++ b/telereddit/linker.py @@ -1,6 +1,13 @@ """Linker class which handles all telereddit requests.""" -from telegram import InputMediaPhoto, InputMediaVideo, InputMediaDocument +from typing import Optional +from telegram import ( # type: ignore + InputMediaPhoto, + InputMediaVideo, + InputMediaDocument, +) +from telegram.bot import Bot, Message # type: ignore +import icontract from telereddit.config.config import ( MAX_TRIES, @@ -12,7 +19,7 @@ ) import telereddit.reddit as reddit import telereddit.helpers as helpers -from telereddit.models.media import ContentType +from telereddit.models.media import ContentType, Media from telereddit.exceptions import ( SubredditError, TeleredditError, @@ -22,6 +29,8 @@ ) +@icontract.invariant(lambda self: self.bot is not None) +@icontract.invariant(lambda self: self.chat_id is not None) class Linker: """ Handle a single telereddit request. @@ -40,8 +49,10 @@ class Linker: """ + bot: Bot = None + @classmethod - def set_bot(cls, bot): + def set_bot(cls, bot: Bot) -> None: """ Set the python-telegram-bot's Bot instance for the Linker object. @@ -57,16 +68,18 @@ def set_bot(cls, bot): """ cls.bot = bot - def __init__(self, chat_id): - self.chat_id = chat_id - self.args = dict( + def __init__(self, chat_id: int) -> None: + self.chat_id: int = chat_id + self.args: dict = dict( chat_id=chat_id, parse_mode="MarkdownV2", reply_markup=EDIT_KEYBOARD, disable_web_page_preview=True, ) - def get_args(self, override_dict={}): + @icontract.snapshot(lambda self: self.args, name="args") + @icontract.ensure(lambda OLD, self, override_dict: OLD.args == self.args) + def get_args(self, override_dict: Optional[dict] = None) -> dict: """ Get the args parameters potentially overriding some of them. @@ -84,10 +97,14 @@ def get_args(self, override_dict={}): """ args = self.args.copy() - args.update(override_dict) + if override_dict: + args.update(override_dict) return args - def send_random_post(self, subreddit): + @icontract.require( + lambda subreddit: subreddit is not None, "subreddit must not be None" + ) + def send_random_post(self, subreddit: str) -> None: """ Send a random post to the chat from the given subreddit. @@ -110,7 +127,10 @@ def send_random_post(self, subreddit): break return self._send_exception_message(err) - def send_post_from_url(self, post_url): + @icontract.require( + lambda post_url: post_url is not None, "post_url must not be None" + ) + def send_post_from_url(self, post_url: str) -> None: """ Try to send the reddit post relative to post_url to the chat. @@ -127,7 +147,10 @@ def send_post_from_url(self, post_url): except TeleredditError as e: self._send_exception_message(e, keyboard=False) - def send_post(self, post_url, from_url=False): + @icontract.require( + lambda post_url: post_url is not None, "post_url must not be None" + ) + def send_post(self, post_url: str, from_url: bool = False) -> None: """ Send the reddit post relative to post_url to the chat. @@ -146,6 +169,7 @@ def send_post(self, post_url, from_url=False): """ post = reddit.get_post(post_url) + assert post is not None if post.media and post.media.size and post.media.size > MAX_MEDIA_SIZE: raise MediaTooBigError() @@ -161,7 +185,8 @@ def send_post(self, post_url, from_url=False): elif post.get_type() == ContentType.YOUTUBE: args["disable_web_page_preview"] = False self.bot.sendMessage(text=post.get_msg(), **args) - elif post.get_type() == ContentType.GIF: + assert post.media is not None + if post.get_type() == ContentType.GIF: self.bot.sendDocument( document=post.media.url, caption=post.get_msg(), **args ) @@ -174,12 +199,15 @@ def send_post(self, post_url, from_url=False): photo=post.media.url, caption=post.get_msg(), **args ) - except Exception: + except Exception as e: raise PostSendError( - {"post_url": post.permalink, "media_url": post.media.url} - ) + {"post_url": post.permalink, "media_url": post.media.url} # type: ignore + ) from e - def edit_result(self, message): + @icontract.require( + lambda message: message is not None, "message must not be None" + ) + def edit_result(self, message: Message) -> None: """ Edit the given message with a new post from that subreddit. @@ -207,7 +235,10 @@ def edit_result(self, message): self.chat_id, message.message_id, reply_markup=EDIT_FAILED_KEYBOARD ) - def edit_random_post(self, message, subreddit): + @icontract.require( + lambda message: message is not None, "message must not be None" + ) + def edit_random_post(self, message: Message, subreddit: str) -> None: """ Edit the current Telegram message with another random Reddit post. @@ -222,7 +253,7 @@ def edit_random_post(self, message, subreddit): """ msg_is_text = message.caption is None post = reddit.get_post(helpers.get_random_post_url(subreddit)) - + assert post is not None if ( (msg_is_text and message.text_markdown == post.get_msg()) or message.caption_markdown == post.get_msg() @@ -241,8 +272,9 @@ def edit_random_post(self, message, subreddit): args["disable_web_page_preview"] = False self.bot.editMessageText(post.get_msg(), **args) else: + media: Optional[Media] = None media_args = dict( - media=post.media.url, + media=post.media.url, # type: ignore caption=post.get_msg(), parse_mode="Markdown", ) @@ -254,12 +286,15 @@ def edit_random_post(self, message, subreddit): media = InputMediaPhoto(**media_args) self.bot.editMessageMedia(media=media, **args) return - except Exception: + except Exception as e: raise PostSendError( - {"post_url": post.permalink, "media_url": post.media.url} - ) + {"post_url": post.permalink, "media_url": post.media.url} # type: ignore + ) from e - def _send_exception_message(self, e, keyboard=True): + @icontract.require(lambda e: e is not None, "e must not be None") + def _send_exception_message( + self, e: Exception, keyboard: bool = True + ) -> None: """ Send the exception text as a Telegram message to notify the user. diff --git a/telereddit/models/content_type.py b/telereddit/models/content_type.py index 5eef9cc..b8c7b74 100644 --- a/telereddit/models/content_type.py +++ b/telereddit/models/content_type.py @@ -6,4 +6,4 @@ class ContentType(Enum): """Enumerable to represent different `telereddit.models.post.Post` types.""" - TEXT, PHOTO, VIDEO, GIF, YOUTUBE, *_ = range(20) + TEXT, PHOTO, VIDEO, GIF, YOUTUBE = range(5) diff --git a/telereddit/models/media.py b/telereddit/models/media.py index 4fa2e01..8efcc35 100644 --- a/telereddit/models/media.py +++ b/telereddit/models/media.py @@ -1,5 +1,6 @@ """Module for Media class.""" +from typing import Optional from telereddit.models.content_type import ContentType @@ -28,7 +29,9 @@ class Media: """ - def __init__(self, url, media_type: ContentType, size=None): + def __init__( + self, url: str, media_type: ContentType, size: Optional[int] = None + ): self.url = url self.type = media_type self.size = size diff --git a/telereddit/models/post.py b/telereddit/models/post.py index b9488d8..dec8b69 100644 --- a/telereddit/models/post.py +++ b/telereddit/models/post.py @@ -1,7 +1,10 @@ """Module for Post class.""" +from typing import Optional + from telereddit.models.content_type import ContentType from telereddit.helpers import escape_markdown +from telereddit.models.media import Media class Post: @@ -26,14 +29,22 @@ class Post: """ - def __init__(self, subreddit, permalink, title, text, media=None): + def __init__( + self, + subreddit: str, + permalink: str, + title: str, + text: str, + media: Optional[Media] = None, + ): + self.subreddit = subreddit self.permalink = permalink self.title = title self.text = text self.media = media - def get_msg(self): + def get_msg(self) -> str: """ Get the full message of the post. @@ -51,6 +62,6 @@ def get_msg(self): f"\n\n{footer}" ) - def get_type(self): + def get_type(self) -> ContentType: """Return the post type: this is determined by the media type, if present.""" return self.media.type if self.media else ContentType.TEXT diff --git a/telereddit/reddit.py b/telereddit/reddit.py index d0d457e..951bb31 100644 --- a/telereddit/reddit.py +++ b/telereddit/reddit.py @@ -5,8 +5,11 @@ and build telereddit objects. """ -import requests import random +from typing import Any + +import requests +import icontract import telereddit.helpers as helpers from telereddit.config.config import secret @@ -22,7 +25,14 @@ from telereddit.services.services_wrapper import ServicesWrapper -def _get_json(post_url): +@icontract.require( + lambda post_url: post_url is not None, "post_url must not be None" +) +@icontract.ensure( + lambda result: helpers.chained_get(result, ["data", "children"])[0]["data"] + is not None +) +def _get_json(post_url: str) -> Any: """ Get post json from Reddit API and handle all request/json errors. @@ -45,12 +55,12 @@ def _get_json(post_url): json = response.json() # some subreddits have the json data wrapped in brackets, some do not json = json if isinstance(json, dict) else json[0] - except Exception: - raise PostRequestError({"post_url": post_url}) + except Exception as e: + raise PostRequestError({"post_url": post_url}) from e if json.get("reason") == "private": raise SubredditPrivateError() - elif ( + if ( json.get("error") == 404 or len(json["data"]["children"]) == 0 or (len(response.history) > 0 and "search.json" in response.url) @@ -61,7 +71,11 @@ def _get_json(post_url): return json -def get_post(post_url): +@icontract.require( + lambda post_url: post_url is not None, "post_url must not be None" +) +@icontract.ensure(lambda result: result is not None) +def get_post(post_url: str) -> Post: """ Get the post from the Reddit API and construct the Post object. @@ -77,6 +91,7 @@ def get_post(post_url): """ json = _get_json(post_url) + assert json is not None try: idx = random.randint(0, len(json["data"]["children"]) - 1) @@ -90,6 +105,7 @@ def get_post(post_url): media = None if "/comments/" not in content_url: media = ServicesWrapper.get_media(content_url, data) + assert media is not None if media.type == ContentType.YOUTUBE: post_text = ( f"{post_text}\n\n[Link to youtube video]({media.url})" @@ -101,4 +117,4 @@ def get_post(post_url): except Exception as e: if issubclass(type(e), TeleredditError): raise e - raise PostRetrievalError({"post_url": post_url}) + raise PostRetrievalError({"post_url": post_url}) from e diff --git a/telereddit/services/generic_service.py b/telereddit/services/generic_service.py index ca98503..fce277b 100644 --- a/telereddit/services/generic_service.py +++ b/telereddit/services/generic_service.py @@ -1,4 +1,6 @@ """Service for when a suitable specific service is not found.""" +from typing import Optional + from telereddit.services.service import Service from telereddit.models.media import Media from telereddit.models.content_type import ContentType @@ -8,10 +10,10 @@ class Generic(Service): """Service for when a suitable specific service is not found.""" @classmethod - def postprocess(cls, response): + def postprocess(cls, response) -> Media: """Override of `telereddit.services.service.Service.postprocess` method.""" - file_size = None - media_type = ContentType.PHOTO + file_size: Optional[int] = None + media_type: ContentType = ContentType.PHOTO if ".gif" in response.url: media_type = ContentType.GIF diff --git a/telereddit/services/gfycat_service.py b/telereddit/services/gfycat_service.py index 57ac382..9e5e1f1 100644 --- a/telereddit/services/gfycat_service.py +++ b/telereddit/services/gfycat_service.py @@ -1,17 +1,24 @@ """Service for Gfycat GIFs.""" +from typing import Any + import json +from urllib.parse import urlparse import requests -from urllib.parse import urlparse -from telereddit.config.config import secret +from requests import Response +import icontract +from telereddit.config.config import secret from telereddit.services.service import Service from telereddit.models.media import Media from telereddit.models.content_type import ContentType from telereddit.exceptions import AuthenticationError +@icontract.invariant( + lambda self: self.is_authenticated is True and self.access_token is not None +) class Gfycat(Service): """ Service for Gfycat GIFs. @@ -22,23 +29,23 @@ class Gfycat(Service): """ - is_authenticated = True + is_authenticated: bool = True def __init__(self): Gfycat.authenticate() @classmethod - def preprocess(cls, url, json): + def preprocess(cls, url: str, data: Any) -> str: """ Override of `telereddit.services.service.Service.preprocess` method. Extracts the gfycat Id from the url and constructs the provider url. """ - gfyid = urlparse(url).path.partition("-")[0] + gfyid: str = urlparse(url).path.partition("-")[0] return f"https://api.gfycat.com/v1/gfycats/{gfyid}" @classmethod - def get(cls, url): + def get(cls, url: str) -> Response: """ Override of `telereddit.services.service.Service.get` method. @@ -49,35 +56,35 @@ def get(cls, url): ) @classmethod - def postprocess(cls, response): + def postprocess(cls, response) -> Media: """ Override of `telereddit.services.service.Service.postprocess` method. Returns the media url which respects the Telegram API file limits, if present. """ - gfy_item = json.loads(response.content)["gfyItem"] - media = Media( + gfy_item: Any = json.loads(response.content)["gfyItem"] + media: Media = Media( gfy_item["webmUrl"].replace(".webm", ".mp4"), ContentType.VIDEO, gfy_item["webmSize"], ) # Telegram does not support webm - # See: https://www.reddit.com/r/Telegram/comments/5wcqh8/sending_webms_as_videos/ - if media.size > 20000000: + # See: https://shorturl.at/dnyBM + if media.size and media.size > 20000000: media.url = gfy_item["max5mbGif"] media.type = ContentType.GIF media.size = 5000000 return media @classmethod - def authenticate(cls): + def authenticate(cls) -> None: """ Override of `telereddit.services.service.Service.authenticate` method. Authenticates the service through OAuth. """ - response = requests.post( + response: Response = requests.post( "https://api.gfycat.com/v1/oauth/token", data=json.dumps( { @@ -88,12 +95,6 @@ def authenticate(cls): ), ) if response.status_code >= 300: - raise AuthenticationError( - { - "response_text": response.text, - # "client_id": secret.GFYCAT_CLIENT_ID, - # "client_secret": secret.GFYCAT_CLIENT_SECRET, - } - ) + raise AuthenticationError({"response_text": response.text}) cls.access_token = json.loads(response.content)["access_token"] diff --git a/telereddit/services/imgur_service.py b/telereddit/services/imgur_service.py index 8eaf88c..cfd1e55 100644 --- a/telereddit/services/imgur_service.py +++ b/telereddit/services/imgur_service.py @@ -1,8 +1,10 @@ """Service for Imgur images and videos.""" +import re +from typing import Any import json from urllib.parse import urlparse import requests -import re +from requests import Response from telereddit.config.config import secret from telereddit.services.service import Service @@ -14,22 +16,22 @@ class Imgur(Service): """Service for Imgur images and videos.""" @classmethod - def preprocess(cls, url, json): + def preprocess(cls, url: str, data: Any) -> str: """ Override of `telereddit.services.service.Service.preprocess` method. Gets the media hash from the url and creates the accepted provider media url. """ - media_hash = urlparse(url).path.rpartition("/")[2] + media_hash: str = urlparse(url).path.rpartition("/")[2] r = re.compile(r"image|gallery").search(url) - api = r.group() if r else "image" + api: str = r.group() if r else "image" if "." in media_hash: media_hash = media_hash.rpartition(".")[0] return f"https://api.imgur.com/3/{api}/{media_hash}" @classmethod - def get(cls, url): + def get(cls, url: str) -> Response: """ Override of `telereddit.services.service.Service.get` method. @@ -41,14 +43,14 @@ def get(cls, url): ) @classmethod - def postprocess(cls, response): + def postprocess(cls, response) -> Media: """ Override of `telereddit.services.service.Service.postprocess` method. Creates the right media object based on the size of provider's media. """ - data = json.loads(response.content)["data"] - media = None + data: Any = json.loads(response.content)["data"] + media: Media if "images" in data: data = data["images"][0] if "image/jpeg" in data["type"] or "image/png" in data["type"]: diff --git a/telereddit/services/service.py b/telereddit/services/service.py index ca3860d..2e1f79b 100644 --- a/telereddit/services/service.py +++ b/telereddit/services/service.py @@ -1,11 +1,16 @@ """Abstract Base static Class for every service.""" -from abc import ABC, abstractmethod +from abc import abstractmethod +from typing import Optional, Any, Union + import requests +from requests import Response +import icontract +from telereddit.models.media import Media from telereddit.exceptions import MediaRetrievalError -class Service(ABC): +class Service(icontract.DBC): """ Abstract Base static Class for every service class. @@ -34,12 +39,12 @@ class Service(ABC): """ - has_external_request = True + has_external_request: bool = True """ True if the service needs to reach out to an external http endpoint, False otherwise. """ - is_authenticated = False + is_authenticated: bool = False """ True if the external request needs to be authenticated (i.e. with an Authorization header), False otherwise. @@ -47,7 +52,7 @@ class Service(ABC): .. note:: This is taken into account only if `has_external_request` is set to True """ - access_token = None + access_token: Optional[str] = None """ Contains the access token for the OAuth authentication if present, None otherwise. @@ -57,7 +62,11 @@ class Service(ABC): """ @classmethod - def preprocess(cls, url, json): + @icontract.require( + lambda cls, url, data: url is not None, "url must not be None" + ) + @icontract.ensure(lambda result: result is not None) + def preprocess(cls, url: str, data: Any) -> str: """ Preprocess the media URL coming from Reddit json. @@ -68,7 +77,7 @@ def preprocess(cls, url, json): ---------- url : str Reddit media URL to preprocess. - json : json + data : json Json from the Reddit API which contains the post data. Used to get fallback media urls for specific services. @@ -81,7 +90,9 @@ def preprocess(cls, url, json): return url @classmethod - def get(cls, url): + @icontract.require(lambda cls, url: url is not None, "url must not be None") + @icontract.ensure(lambda result: result is not None) + def get(cls, url: str) -> Union[Response, str]: """ Get the media information. @@ -107,7 +118,11 @@ def get(cls, url): @classmethod @abstractmethod - def postprocess(cls, response): + @icontract.require( + lambda cls, response: response is not None, "response must not be None" + ) + @icontract.ensure(lambda result: result is not None) + def postprocess(cls, response: Union[Response, str]) -> Media: """ From the service provider API response create the media object. @@ -130,17 +145,20 @@ def postprocess(cls, response): raise NotImplementedError() @classmethod - def authenticate(cls): + def authenticate(cls) -> None: """ Authenticate the service on the service provider API. Update the `access_code` variable with the newly refreshed valid access token. """ - pass @classmethod - def get_media(cls, url, json): + @icontract.require( + lambda cls, url, data: url is not None, "url must not be None" + ) + @icontract.ensure(lambda result: result is not None) + def get_media(cls, url: str, data: Any) -> Media: """ Entrypoint of the class. @@ -162,7 +180,7 @@ def get_media(cls, url, json): ---------- url : str Media URL from the Reddit API json. - json : json + data : json Json from the Reddit API which contains the post data. Used to get fallback media urls for specific services. @@ -172,14 +190,14 @@ def get_media(cls, url, json): The media object accessible from the application. """ - processed_url = cls.preprocess(url, json) + processed_url: str = cls.preprocess(url, data) - response = cls.get(processed_url) + response: Union[Response, str] = cls.get(processed_url) if cls.has_external_request: - if cls.is_authenticated and response.status_code == 401: + if cls.is_authenticated and response.status_code == 401: # type: ignore cls.authenticate() response = cls.get(processed_url) - if response.status_code >= 300: + if response.status_code >= 300: # type: ignore raise MediaRetrievalError( { "service": cls.__name__, diff --git a/telereddit/services/services_wrapper.py b/telereddit/services/services_wrapper.py index 58abaf4..956f848 100644 --- a/telereddit/services/services_wrapper.py +++ b/telereddit/services/services_wrapper.py @@ -2,12 +2,15 @@ import logging from urllib.parse import urlparse +from typing import Any +import icontract from telereddit.services.gfycat_service import Gfycat from telereddit.services.vreddit_service import Vreddit from telereddit.services.imgur_service import Imgur from telereddit.services.youtube_service import Youtube from telereddit.services.generic_service import Generic +from telereddit.models.media import Media class ServicesWrapper: @@ -20,14 +23,18 @@ class ServicesWrapper: An instance for each service class is set at class initialization. """ - gfycat = Gfycat() - vreddit = Vreddit() - imgur = Imgur() - youtube = Youtube() - generic = Generic() + gfycat: Gfycat = Gfycat() + vreddit: Vreddit = Vreddit() + imgur: Imgur = Imgur() + youtube: Youtube = Youtube() + generic: Generic = Generic() @classmethod - def get_media(cls, url, json={}): + @icontract.require( + lambda cls, url, data: url is not None, "url must not be None" + ) + @icontract.ensure(lambda result: result is not None) + def get_media(cls, url: str, data: Any = None) -> Media: """ Given the url from the Reddit json, return the corresponding media obj. @@ -37,7 +44,7 @@ def get_media(cls, url, json={}): ---------- url : str Url from Reddit API json. - json : json + data : json (Default value = {}) Reddit data json containing media fallback urls. @@ -48,21 +55,22 @@ def get_media(cls, url, json={}): The media object corresponding to the media post url. """ - parsed_url = urlparse(url) - base_url = parsed_url.netloc + base_url: str = urlparse(url).netloc + media: Media if "gfycat.com" in base_url: - media = cls.gfycat.get_media(url, json) + media = cls.gfycat.get_media(url, data) elif "v.redd.it" in base_url: - media = cls.vreddit.get_media(url, json) + media = cls.vreddit.get_media(url, data) elif "imgur.com" in base_url: - media = cls.imgur.get_media(url, json) + media = cls.imgur.get_media(url, data) elif "youtube.com" in base_url or "youtu.be" in base_url: - media = cls.youtube.get_media(url, json) + media = cls.youtube.get_media(url, data) else: - logging.warning( - f"services_wrapper: no suitable service found. base_url: {base_url}" + logging.info( + "services_wrapper: no suitable service found. base_url: %s", + base_url, ) - media = cls.generic.get_media(url, json) + media = cls.generic.get_media(url, data) return media diff --git a/telereddit/services/vreddit_service.py b/telereddit/services/vreddit_service.py index 980c66a..294b928 100644 --- a/telereddit/services/vreddit_service.py +++ b/telereddit/services/vreddit_service.py @@ -1,4 +1,5 @@ """Service for v.redd.it GIFs.""" +from typing import Any, Optional import requests from telereddit.services.service import Service @@ -11,7 +12,7 @@ class Vreddit(Service): """Service for v.redd.it GIFs.""" @classmethod - def preprocess(cls, url, json): + def preprocess(cls, url: str, data: Any) -> str: """ Override of `telereddit.services.service.Service.preprocess` method. @@ -21,7 +22,8 @@ def preprocess(cls, url, json): needed, therefore we need to seach in the json for the correct piece of information in every specific case. """ - xpost = helpers.get(json, "crosspost_parent_list") + xpost: Optional[Any] = helpers.get(data, "crosspost_parent_list") + fallback_url: str if xpost is not None and len(xpost) > 0: # crossposts have media = null and have the fallback url in the # crosspost source @@ -30,22 +32,24 @@ def preprocess(cls, url, json): ) else: fallback_url = helpers.chained_get( - json, ["media", "reddit_video", "fallback_url"] + data, ["media", "reddit_video", "fallback_url"] ) - processed_url = fallback_url if fallback_url else f"{url}/DASH_1_2_M" + processed_url: str = ( + fallback_url if fallback_url else f"{url}/DASH_1_2_M" + ) if requests.head(processed_url).status_code >= 300: processed_url = f"{url}/DASH_1080" return processed_url @classmethod - def postprocess(cls, response): + def postprocess(cls, response) -> Media: """ Override of `telereddit.services.service.Service.postprocess` method. Constructs the media object. """ - media = Media(response.url, ContentType.GIF) + media: Media = Media(response.url, ContentType.GIF) if "Content-length" in response.headers: media.size = int(response.headers["Content-length"]) return media diff --git a/telereddit/services/youtube_service.py b/telereddit/services/youtube_service.py index 004bf13..f66cd2e 100644 --- a/telereddit/services/youtube_service.py +++ b/telereddit/services/youtube_service.py @@ -1,4 +1,6 @@ """Service for Youtube URLs.""" +from typing import Optional, Any + from telereddit.services.service import Service from telereddit.models.media import Media from telereddit.models.content_type import ContentType @@ -16,22 +18,22 @@ class Youtube(Service): """ - access_token = None - is_authenticated = False - has_external_request = False + access_token: Optional[str] = None + is_authenticated: bool = False + has_external_request: bool = False @classmethod - def preprocess(cls, url, json): + def preprocess(cls, url: str, data: Any) -> str: """ Override of `telereddit.services.service.Service.preprocess` method. Gets the youtube url from reddit json. """ - oembed_url = helpers.chained_get(json, ["media", "oembed", "url"]) + oembed_url: str = helpers.chained_get(data, ["media", "oembed", "url"]) return oembed_url if oembed_url else url @classmethod - def get(cls, url): + def get(cls, url: str) -> str: """ Override of `telereddit.services.service.Service.get` method. @@ -40,10 +42,10 @@ def get(cls, url): return url @classmethod - def postprocess(cls, url): + def postprocess(cls, response) -> Media: """ Override of `telereddit.services.service.Service.postprocess` method. Constructs the media object. """ - return Media(url, ContentType.YOUTUBE) + return Media(response, ContentType.YOUTUBE) diff --git a/telereddit/telereddit.py b/telereddit/telereddit.py index f511eff..51e72a4 100644 --- a/telereddit/telereddit.py +++ b/telereddit/telereddit.py @@ -7,17 +7,20 @@ messages and dispatch actions to the other modules. """ +import logging +from typing import List + import sentry_sdk -from telegram.ext import ( +from telegram.ext import ( # type: ignore Updater, CallbackContext, MessageHandler, CallbackQueryHandler, Filters, ) -from telegram import Update -import logging +from telegram import Update, Message # type: ignore +import icontract from telereddit.linker import Linker import telereddit.helpers as helpers @@ -25,7 +28,13 @@ from telereddit.config.config import secret -def on_chat_message(update: Update, context: CallbackContext): +@icontract.require( + lambda update, context: update is not None, "update must not be None" +) +@icontract.require( + lambda update, context: context is not None, "context must not be None" +) +def on_chat_message(update: Update, context: CallbackContext) -> None: """ Entrypoint of the bot's logic. Handles a single update message. @@ -37,23 +46,29 @@ def on_chat_message(update: Update, context: CallbackContext): The Context object provided by python-telegram-bot """ - msg = update.message + msg: Message = update.message if not msg or not msg.text: return - linker = Linker(msg.chat_id) - text = msg.text.lower() + linker: Linker = Linker(msg.chat_id) + text: str = msg.text.lower() if any(r in text for r in config.REDDIT_DOMAINS): - posts_url = helpers.get_urls_from_text(msg.text) + posts_url: List[str] = helpers.get_urls_from_text(msg.text) for url in posts_url: linker.send_post_from_url(url) elif "r/" in text: - subreddits = helpers.get_subreddit_names(text) + subreddits: List[str] = helpers.get_subreddit_names(text) for subreddit in subreddits: linker.send_random_post(subreddit) -def on_callback_query(update: Update, context: CallbackContext): +@icontract.require( + lambda update, context: update is not None, "update must not be None" +) +@icontract.require( + lambda update, context: context is not None, "context must not be None" +) +def on_callback_query(update: Update, context: CallbackContext) -> None: """ Handle all the several types of callback queries. @@ -75,7 +90,8 @@ def on_callback_query(update: Update, context: CallbackContext): linker = Linker(message.chat_id) if query_data == "more": subreddit = helpers.get_subreddit_name(text, reverse=True) - linker.send_random_post(subreddit) + if subreddit: + linker.send_random_post(subreddit) elif query_data == "edit": linker.edit_result(message) elif query_data == "delete": @@ -84,7 +100,7 @@ def on_callback_query(update: Update, context: CallbackContext): context.bot.answerCallbackQuery(update.callback_query.id) -def main(): +def main() -> None: """Entrypoint of telereddit. Handles configuration, setup and start of the bot.""" if config.SENTRY_ENABLED: sentry_sdk.init(secret.SENTRY_TOKEN, environment=config.ENV) diff --git a/telereddit/tests/test_helpers.py b/telereddit/tests/test_helpers.py index a2c7bd2..7134a67 100644 --- a/telereddit/tests/test_helpers.py +++ b/telereddit/tests/test_helpers.py @@ -11,6 +11,10 @@ def test_get_random_post_url(self): "https://www.reddit.com/r/gifs/random", ) + def test_get_random_post_url_invalid(self): + with self.assertRaises(Exception): + helpers.get_random_post_url() + @parameterized.expand( [ # subreddit in a text @@ -73,24 +77,29 @@ def test_get_subreddit_names_notvalid(self, text): param( text="r/subreddit_one", reverse=True, expected="r/subreddit_one" ), - param(text="", expected=None), ] ) def test_get_subreddit_name(self, text, expected, reverse=False): self.assertEqual(helpers.get_subreddit_name(text, reverse), expected) + def test_get_subreddit_name_invalid(self): + with self.assertRaises(Exception): + helpers.get_subreddit_name("") + @parameterized.expand( [ param(text="truncate me", length=7, expected="truncat..."), param(text="truncate me", length=11, expected="truncate me"), - param(text="truncate me", length=0, expected="..."), - param(text="truncate me", length=-1, expected="truncate me"), param(text="", length=10, expected=""), ] ) def test_truncate_text(self, text, length, expected): self.assertEqual(helpers.truncate_text(text, length), expected) + def test_truncate_invalid(self): + with self.assertRaises(Exception): + helpers.truncate_text("", 0) + @parameterized.expand( [ param(text="", expected=""), diff --git a/telereddit/tests/test_linker.py b/telereddit/tests/test_linker.py index 62b5255..5c8fe78 100644 --- a/telereddit/tests/test_linker.py +++ b/telereddit/tests/test_linker.py @@ -1,12 +1,27 @@ import unittest +from unittest.mock import patch, Mock +from parameterized import parameterized + from telereddit.linker import Linker +from telereddit.exceptions import ( + TeleredditError, + SubredditError, + PostEqualsMessageError, +) +from telereddit.models.post import Post +from telereddit.models.media import Media +from telereddit.models.content_type import ContentType +from telereddit.config.config import MAX_MEDIA_SIZE class TestLinker(unittest.TestCase): - Linker.set_bot(None) + Linker.set_bot(Mock()) linker = Linker(0) + def test_bot_not_none(self): + self.assertIsNotNone(self.linker.bot) + def test_get_args(self): args = self.linker.get_args() self.assertIn("chat_id", args) @@ -14,3 +29,157 @@ def test_get_args(self): def test_get_args_override(self): self.assertIn("test", self.linker.get_args({"test": True})) + + @patch("telereddit.linker.Linker.send_post") + @patch("telereddit.linker.Linker._send_exception_message") + def test_send_random_post(self, mock_err_function, mock_send_post): + self.linker.send_random_post("r/valid") + mock_err_function.assert_not_called() + + @patch("telereddit.linker.Linker.send_post") + @patch("telereddit.linker.Linker._send_exception_message") + def test_send_random_post_invalid(self, mock_err_function, mock_send_post): + mock_send_post.side_effect = TeleredditError("") + self.linker.send_random_post("r/invalid") + mock_err_function.assert_called() + + mock_send_post.side_effect = SubredditError("") + self.linker.send_random_post("r/invalid") + mock_err_function.assert_called() + + @patch("telereddit.linker.Linker.send_post") + @patch("telereddit.linker.Linker._send_exception_message") + def test_send_post_from_url(self, mock_err_function, mock_send_post): + self.linker.send_post_from_url("r/valid") + mock_err_function.assert_not_called() + + @patch("telereddit.linker.Linker.send_post") + @patch("telereddit.linker.Linker._send_exception_message") + def test_send_post_from_url_invalid( + self, mock_err_function, mock_send_post + ): + mock_send_post.side_effect = TeleredditError("") + self.linker.send_post_from_url("") + mock_err_function.assert_called() + + @parameterized.expand( + [ + [ContentType.PHOTO], + [ContentType.GIF], + [ContentType.VIDEO], + [ContentType.YOUTUBE], + [ContentType.TEXT], + ] + ) + @patch("telereddit.reddit.get_post") + def test_send_post(self, mock_content_type, mock_get_post): + media = Media("", mock_content_type) + mock_get_post.return_value = Post("", "", "", "", media) + self.linker.send_post("") + + @patch("telereddit.reddit.get_post") + def test_send_post_no_type(self, mock_get_post): + media = Media("", None) + mock_get_post.return_value = Post("", "", "", "", media) + self.linker.send_post("") + + @patch("telereddit.reddit.get_post") + def test_send_post_from_url_true(self, mock_get_post): + media = Media("", ContentType.PHOTO) + mock_get_post.return_value = Post("", "", "", "", media) + self.linker.send_post("", from_url=True) + + @patch("telereddit.reddit.get_post") + def test_send_post_invalid(self, mock_get_post): + mock_get_post.side_effect = TeleredditError("") + with self.assertRaises(TeleredditError): + self.linker.send_post("") + + @patch("telereddit.reddit.get_post") + def test_send_post_media_too_big(self, mock_get_post): + media = Media("", ContentType.PHOTO, size=MAX_MEDIA_SIZE + 1) + mock_get_post.return_value = Post("", "", "", "", media) + with self.assertRaises(TeleredditError): + self.linker.send_post("") + + @patch("telereddit.linker.Linker.bot.sendMessage") + @patch("telereddit.reddit.get_post") + def test_send_post_err(self, mock_get_post, mock_send_message): + mock_send_message.side_effect = TeleredditError("") + media = Media("", ContentType.TEXT) + mock_get_post.return_value = Post("", "", "", "", media) + with self.assertRaises(TeleredditError): + self.linker.send_post("") + + def test_edit_result_none(self): + mock_msg = Mock() + mock_msg.text = "" + mock_msg.caption = "" + self.linker.edit_result(mock_msg) + + @patch("telereddit.linker.Linker.edit_random_post") + def test_edit_result(self, mock_edit_random_post): + mock_msg = Mock() + mock_msg.text = "r/funny" + mock_msg.caption = "" + self.linker.edit_result(mock_msg) + + @patch("telereddit.linker.Linker.edit_random_post") + def test_edit_result_invalid(self, mock_edit_random_post): + mock_edit_random_post.side_effect = TeleredditError("") + mock_msg = Mock() + mock_msg.text = "r/funny" + mock_msg.caption = "" + self.linker.edit_result(mock_msg) + + @patch("telereddit.reddit.get_post") + def test_edit_random_post_text(self, mock_get_post): + media = Media("", ContentType.TEXT) + mock_get_post.return_value = Post("", "", "", "", media) + mock_msg = Mock() + mock_msg.text = "" + mock_msg.caption = None + self.linker.edit_random_post(mock_msg, "r/test") + + @patch("telereddit.reddit.get_post") + def test_edit_random_post_invalid(self, mock_get_post): + media = Media("", ContentType.TEXT) + mock_get_post.return_value = Post("", "", "", "", media) + mock_msg = Mock() + mock_msg.text = None + mock_msg.caption = "" + with self.assertRaises(PostEqualsMessageError): + self.linker.edit_random_post(mock_msg, "r/test") + + @patch("telereddit.reddit.get_post") + def test_edit_random_post_youtube(self, mock_get_post): + media = Media("", ContentType.YOUTUBE) + mock_get_post.return_value = Post("", "", "", "", media) + mock_msg = Mock() + mock_msg.text = "" + mock_msg.caption = None + mock_msg.caption_markdown = "" + self.linker.edit_random_post(mock_msg, "r/test") + + @parameterized.expand( + [[ContentType.PHOTO], [ContentType.GIF], [ContentType.VIDEO]] + ) + @patch("telereddit.reddit.get_post") + def test_edit_random_post_types(self, mock_type, mock_get_post): + media = Media("", mock_type) + mock_get_post.return_value = Post("", "", "", "", media) + mock_msg = Mock() + mock_msg.text = None + mock_msg.caption = "" + mock_msg.caption_markdown = "" + self.linker.edit_random_post(mock_msg, "r/test") + + @patch("telereddit.linker.Linker.bot.sendMessage") + def test_send_exception_message(self, mock_send_message): + e = Mock() + self.linker._send_exception_message(e) + + @patch("telereddit.linker.Linker.bot.sendMessage") + def test_send_exception_message_no_kb(self, mock_send_message): + e = Mock() + self.linker._send_exception_message(e, False) diff --git a/telereddit/tests/test_reddit.py b/telereddit/tests/test_reddit.py index 584429a..378fe3a 100644 --- a/telereddit/tests/test_reddit.py +++ b/telereddit/tests/test_reddit.py @@ -70,6 +70,20 @@ def test_get_json_valid(self, mock_get): ) self.assertEqual(json_data, mock_json[0]) + @patch("telereddit.reddit.requests.get") + def test_get_json(self, mock_get): + mock_get.return_value.ok = True + mock_get.return_value.status_code = 200 + mock_get.return_value.history = [0] + mock_get.return_value.json = lambda: { + "data": {"children": [{"mock": True}]} + } + mock_get.return_value.url = "https://www.reddit.com/search.json" + with self.assertRaises(SubredditDoesntExistError): + reddit._get_json( + "https://www.reddit.com/r/funny/comments/fxuefa/my_weather_app_nailed_it_today.json" + ) + @parameterized.expand( [ # text post diff --git a/telereddit/tests/test_services.py b/telereddit/tests/test_services.py index e788806..58f26c9 100644 --- a/telereddit/tests/test_services.py +++ b/telereddit/tests/test_services.py @@ -151,6 +151,15 @@ def test_generic(self, url, expected_url, expected_type): self.assertEqual(media.url, expected_url) self.assertEqual(media.type, expected_type) + @patch("telereddit.services.service.requests.get") + def test_generic_video(self, mock_get): + mock_get.return_value.url = "https://mock.video.url/filename.mp4" + mock_get.return_value.status_code = 200 + mock_get.headers = None + media = ServicesWrapper.get_media("https://test.test.test") + self.assertIsNone(media.size) + self.assertEqual(media.type, ContentType.VIDEO) + @patch("telereddit.services.gfycat_service.requests.post") def test_gfycat_authentication_fail(self, mock_post): mock_post.return_value.status_code = 401