From 896d3b0cf61bf6defdcaf7f2efef78dc8123a87b Mon Sep 17 00:00:00 2001 From: Kevo Date: Sat, 26 Feb 2022 10:35:58 +0100 Subject: [PATCH] Comments --- .github/workflows/build_documentation.yml | 2 +- .github/workflows/publish_to_pypi.yml | 2 +- .github/workflows/pytest.yml | 4 +- .gitignore | 5 +- .vscode/launch.json | 16 --- README.md | 44 +------ piped_api/__init__.py | 12 ++ piped_api/client.py | 64 ++++++++++ piped_api/models/__init__.py | 15 +++ piped_api/models/comments.py | 141 ++++++++++++++++++++++ python_project_template/__init__.py | 25 ---- requirements.txt | 2 + scripts/build_documentation.sh | 2 +- setup.py | 10 +- tests/__init__.py | 4 + tests/test_comments.py | 34 ++++++ tests/test_infinite_random_numbers.py | 12 -- 17 files changed, 286 insertions(+), 108 deletions(-) delete mode 100644 .vscode/launch.json create mode 100644 piped_api/__init__.py create mode 100644 piped_api/client.py create mode 100644 piped_api/models/__init__.py create mode 100644 piped_api/models/comments.py delete mode 100644 python_project_template/__init__.py create mode 100644 tests/__init__.py create mode 100644 tests/test_comments.py delete mode 100644 tests/test_infinite_random_numbers.py diff --git a/.github/workflows/build_documentation.yml b/.github/workflows/build_documentation.yml index 4b58d1d..e616081 100644 --- a/.github/workflows/build_documentation.yml +++ b/.github/workflows/build_documentation.yml @@ -35,4 +35,4 @@ jobs: uses: JamesIves/github-pages-deploy-action@4.1.4 with: branch: documentation - folder: ./documentation + folder: ./documentation \ No newline at end of file diff --git a/.github/workflows/publish_to_pypi.yml b/.github/workflows/publish_to_pypi.yml index 4aad18f..40783c7 100644 --- a/.github/workflows/publish_to_pypi.yml +++ b/.github/workflows/publish_to_pypi.yml @@ -14,7 +14,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: '^3.6' + python-version: '^3.10' - name: Install dependencies run: | diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 89a41d9..aadcac6 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -12,7 +12,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: '^3.6' + python-version: '^3.10' - name: Install dependencies run: | @@ -22,4 +22,4 @@ jobs: - name: Test with pyTest run: | - python -m pytest -v + python -m pytest -v \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1e94e76..d8faf46 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,8 @@ -# TODO: Your editor's folder -# .vscode/ +.vscode/ __pycache__/ env/ -.pytest-cache +.pytest-cache/ documentation/ dist/* diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 9589b6d..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - // Pre-made Python debugger (automatically sets `PYTHONPATH` to workspace root directory) - { - "name": "Python: Current File", - "type": "python", - "request": "launch", - "program": "${file}", - "env": { - "PYTHONPATH": "${workspaceRoot}" - }, - "console": "integratedTerminal" - } - ] -} \ No newline at end of file diff --git a/README.md b/README.md index 75f6e89..eb4c6f6 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,6 @@ -# Python project template +# Piped API client (Python) -A template for new Python projects. - -## Features - -- Automatically builds [PDoc](https://pdoc3.github.io/pdoc/) documentation & uploads package to [PyPI](https://pypi.org) on new GitHub release, thanks to GitHub actions; -- Tests with pyTest before uploading to PyPI (or you can test manually with `workflow-dispatch`); -- Ready-to-go `setup.py` file; -- Scripts to build documentation and compile as a Python package; -- A to-do list below; -- Possibly more ;) - -## Your to-do list - -(*Approx. time to set up:* 15 - 25 minutes) - -- [ ] Edit `# FIXME` lines to match your project; - - [ ] setup.py - - [ ] Package name - - [ ] License - - [ ] Version - - [ ] Author - - [ ] Author email - - [ ] Description - - [ ] Keywords - - [ ] Classifiers - - [ ] Repository URL -- [ ] Setup virtualenv (`scripts/setup_virtualenv_windows.ps1` for Windows); -- [ ] Rename `python_project_template` folder and start writing your source code; -- [ ] Add your dependencies to `requirements.txt`; -- [ ] Update .gitingore with your stuff; -- [ ] Replace this `README.md` file with a fancier one; -- [ ] Upload code to your GitHub repository; -- [ ] Turn on GitHub pages and use `documentation` as your pages branch; -- [ ] Add your editior to `.gitignore`; -- [ ] Add your PyPI API key to GitHub secrets (`PYPI_API_TOKEN`); -- [ ] When your are done, make a new release at GitHub to build documentation and upload to PyPI; - - Don't forget to bump version in `setup.py` everytime you do a new release!!! - -That should be it. Happy coding! - -If you have any questions or found a bug, please open a new issue in this repository. +A Python API wrapper for [Piped](https://piped-docs.kavin.rocks/). ## 🎁 Support me diff --git a/piped_api/__init__.py b/piped_api/__init__.py new file mode 100644 index 0000000..947c45d --- /dev/null +++ b/piped_api/__init__.py @@ -0,0 +1,12 @@ +import typing as t + + +from .client import PipedClient + +from .models.comments import Comments + + + +# Supress unused-import warnings: +if t.TYPE_CHECKING: + _ = [PipedClient, Comments] diff --git a/piped_api/client.py b/piped_api/client.py new file mode 100644 index 0000000..27d5b77 --- /dev/null +++ b/piped_api/client.py @@ -0,0 +1,64 @@ +import typing as t + +from requests import Session + +from .models import BasePipedModel +from .models.comments import Comments + + +_MDL = t.TypeVar('_MDL', bound=BasePipedModel) + + + +class PipedClient: + """ + An API client for [Piped](https://piped.kavin.rocks). + """ + + def __init__(self, base_api_url: str='https://pipedapi.kavin.rocks', session: t.Type[Session]=Session()) -> None: + """ + ### Parameters: + - `base_api_url` - The base URL to the instance's API. Trailing slashes will be stripped. + - `session` - A class/subclass of `requests.Session` to use for all requests. + For example, you could use [requests-cache](https://pypi.org/project/requests-cache/) to make all requests cacheable. + """ + + self.base_api_url = base_api_url.strip("/") + self.session = session + + + + def _get_json(self, uri: str, as_model: t.Optional[_MDL]=None, **kwargs) -> t.Union[_MDL, t.Dict[str, t.Any]]: + """ + Obtains JSON data from specific URI of the Piped API. + + ### Parameters: + - `uri` - The URI to get JSON data from + - `as_model` - The `BasePipedModel` to load the JSON data into. If this is `None`, the JSON data is returned as a `dict`. + - `**kwargs` - Additional keyword arguments to pass to `requests.Session.get` + """ + + json = self.session.get(f"{self.base_api_url}{uri}", **kwargs).json() + + if as_model is not None: + return as_model(json) + + return json + + + + def get_comments(self, video_id: str, nextpage: t.Optional[t.Dict[str, t.Optional[str]]]=None) -> Comments: + """ + Gets a list of comments for a specific video. + + ### Parameters: + - `video_id` - The ID of the video to get comments for + - `nextpage` - Nextpage data, obtained from `.models.comments.Comments.nextpage` property. If this is `None`, the first page of comments is returned. + There are often 20 comments per page. + """ + + if nextpage is not None: + return self._get_json(f"/nextpage/comments/{video_id}", Comments, params={"nextpage": nextpage}) + + else: + return self._get_json(f"/comments/{video_id}", Comments) diff --git a/piped_api/models/__init__.py b/piped_api/models/__init__.py new file mode 100644 index 0000000..8f4dfe3 --- /dev/null +++ b/piped_api/models/__init__.py @@ -0,0 +1,15 @@ +import typing as t + + +class BasePipedModel: + """ + Base class for all Piped models. + """ + + def __init__(self, data: t.Dict[str, t.Any]) -> None: + """ + ### Parameters: + - `data` - The JSON (`dict`) data to initialize the model with. + """ + + self.data = data diff --git a/piped_api/models/comments.py b/piped_api/models/comments.py new file mode 100644 index 0000000..2393d21 --- /dev/null +++ b/piped_api/models/comments.py @@ -0,0 +1,141 @@ +import typing as t + +from . import BasePipedModel + + +class Comments(BasePipedModel): + class Comment(BasePipedModel): + @property + def author(self) -> str: + """ + The name of the author of the comment + """ + + return self.data['author'] + + + @property + def comment_id(self) -> str: + """ + The comment ID + """ + + return self.data['commentId'] + + + @property + def comment_text(self) -> str: + """ + The text of the comment + """ + + return self.data['commentText'] + + + @property + def commented_time(self) -> str: + """ + The time the comment was made (format: `'x y ago'`) + """ + + return self.data['commentedTime'] + + + @property + def commentor_url(self) -> str: + """ + The URL of the channel that made the comment + """ + + return self.data['commentorUrl'] + + + @property + def replies_page(self) -> t.Optional[str]: + """ + Same as `Comments.nextpage`, but to load replies. + + `None` means that there are no replies. + """ + + return self.data['repliesPage'] + + + @property + def hearted(self) -> bool: + """ + Whether or not the comment has been hearted + """ + + return self.data['hearted'] + + + @property + def like_count(self) -> int: + """ + The number of likes the comment has + """ + + return self.data['likeCount'] + + + @property + def pinned(self) -> bool: + """ + Whether or not the comment is pinned + """ + + return self.data['pinned'] + + + @property + def thumbnail(self) -> str: + """ + The thumbnail of the commentor's channel + """ + + return self.data['thumbnail'] + + + @property + def verified(self) -> bool: + """ + Whether or not the author of the comment is verified + """ + + return self.data['verified'] + + + + def get_comments(self) -> t.List[Comment]: + """ + Obtain a list of comments + """ + + return [self.Comment(comment_json) for comment_json in self.data['comments']] + + + def __iter__(self) -> t.Iterator[Comment]: + iter(self.get_comments()) + + + + @property + def disabled(self) -> bool: + """ + Whether or not the comments are disabled + """ + + return self.data['disabled'] + + + + @property + def nextpage(self) -> t.Optional[str]: + """ + A JSON encoded page, which is used for the nextpage endpoint. + + If there is no nextpage data, this returns `None`. + """ + + return self.data['nextpage'] diff --git a/python_project_template/__init__.py b/python_project_template/__init__.py deleted file mode 100644 index a72b369..0000000 --- a/python_project_template/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from typing import Generator -from random import random as rand - - - -def infinitum(multiplier: int = 314) -> Generator[int, None, None]: - """ - Generates an infinite sequence of random numbers. - """ - - while True: - yield round(((rand() + 1) ** multiplier) % 21768543) - - - -if __name__ == "__main__": - count = 0 - max_count = 10 - - for random in infinitum(): - print(random) - count += 1 - - if count >= max_count: - break diff --git a/requirements.txt b/requirements.txt index e079f8a..3d76b0f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ pytest + +requests diff --git a/scripts/build_documentation.sh b/scripts/build_documentation.sh index d518426..219ff9b 100644 --- a/scripts/build_documentation.sh +++ b/scripts/build_documentation.sh @@ -1,4 +1,4 @@ cd "$(dirname "$0")" cd ../ -pdoc -o documentation python_project_template # FIXME +pdoc -o documentation piped_api diff --git a/setup.py b/setup.py index 0e20bda..360a6e4 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ sys.path.append('.') import setuptools -__description__ = "Python Project Template" # FIXME +__description__ = "Piped API client" __author__ = "SKevo" __copyright__ = "Copyright (c) 2021, SKevo" __credits__ = ["SKevo"] @@ -31,7 +31,7 @@ __doc__ = __readme__ setuptools.setup( - name = 'python_project_template', # FIXME + name = 'piped_api', packages = setuptools.find_packages(exclude=('tests',)), long_description=__readme__, @@ -40,14 +40,14 @@ setuptools.setup( version = __version__, license = __license__, description = __description__, - keywords = [], # FIXME + keywords = ["piped", "api", "client"], author = __author__, author_email = __email__, - url = 'https://github.com/CWKevo/python-project-template', # FIXME + url = 'https://github.com/CWKevo/python-piped-api-client', - install_requires=[], # FIXME + install_requires=['requests'], classifiers=[ f'Development Status :: {__status__}', diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..a03651c --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,4 @@ +from piped_api import PipedClient + + +CLIENT = PipedClient() diff --git a/tests/test_comments.py b/tests/test_comments.py new file mode 100644 index 0000000..91b3ed0 --- /dev/null +++ b/tests/test_comments.py @@ -0,0 +1,34 @@ +from tests import CLIENT + + +def test_comments(video_id: str='dQw4w9WgXcQ') -> None: + """ + Prints out first 20 pages of comments from a video. + """ + + at_page = 0 + max_pages = 5 + total_comments = 0 + np = None + + while at_page < max_pages: + comments = CLIENT.get_comments(video_id, nextpage=np) + at_page += 1 + + print('=' * 35, f'Page: {at_page}', '=' * 35) + for comment in comments.get_comments(): + total_comments += 1 + print(f'Comment {comment.comment_id} by "{comment.author}" ({comment.commented_time}), {comment.like_count} likes: "{comment.comment_text}"') + + if comments.nextpage == None: + print(f"No more comments! Total: {total_comments}, expected: {max_pages * 20}") + break + + np = comments.nextpage + + print(f"Okay, that's enough comments... Total: {total_comments}, expected: {max_pages * 20}") + + + +if __name__ == '__main__': + test_comments() diff --git a/tests/test_infinite_random_numbers.py b/tests/test_infinite_random_numbers.py deleted file mode 100644 index d05d2e4..0000000 --- a/tests/test_infinite_random_numbers.py +++ /dev/null @@ -1,12 +0,0 @@ -from python_project_template import infinitum - - - -def test_infinitum(): - """ - Tests the infinite random number generator. - """ - - for random in infinitum(): - assert type(random) == int - break