Comments
This commit is contained in:
parent
fc8cc85a04
commit
896d3b0cf6
17 changed files with 286 additions and 108 deletions
2
.github/workflows/publish_to_pypi.yml
vendored
2
.github/workflows/publish_to_pypi.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: '^3.6'
|
python-version: '^3.10'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|
2
.github/workflows/pytest.yml
vendored
2
.github/workflows/pytest.yml
vendored
|
@ -12,7 +12,7 @@ jobs:
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: '^3.6'
|
python-version: '^3.10'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,9 +1,8 @@
|
||||||
# TODO: Your editor's folder
|
.vscode/
|
||||||
# .vscode/
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
env/
|
env/
|
||||||
|
|
||||||
.pytest-cache
|
.pytest-cache/
|
||||||
documentation/
|
documentation/
|
||||||
|
|
||||||
dist/*
|
dist/*
|
||||||
|
|
16
.vscode/launch.json
vendored
16
.vscode/launch.json
vendored
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
44
README.md
44
README.md
|
@ -1,46 +1,6 @@
|
||||||
# Python project template
|
# Piped API client (Python)
|
||||||
|
|
||||||
A template for new Python projects.
|
A Python API wrapper for [Piped](https://piped-docs.kavin.rocks/).
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|
||||||
## 🎁 Support me
|
## 🎁 Support me
|
||||||
|
|
||||||
|
|
12
piped_api/__init__.py
Normal file
12
piped_api/__init__.py
Normal file
|
@ -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]
|
64
piped_api/client.py
Normal file
64
piped_api/client.py
Normal file
|
@ -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)
|
15
piped_api/models/__init__.py
Normal file
15
piped_api/models/__init__.py
Normal file
|
@ -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
|
141
piped_api/models/comments.py
Normal file
141
piped_api/models/comments.py
Normal file
|
@ -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']
|
|
@ -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
|
|
|
@ -1 +1,3 @@
|
||||||
pytest
|
pytest
|
||||||
|
|
||||||
|
requests
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
cd ../
|
cd ../
|
||||||
|
|
||||||
pdoc -o documentation python_project_template # FIXME
|
pdoc -o documentation piped_api
|
||||||
|
|
10
setup.py
10
setup.py
|
@ -6,7 +6,7 @@ sys.path.append('.')
|
||||||
import setuptools
|
import setuptools
|
||||||
|
|
||||||
|
|
||||||
__description__ = "Python Project Template" # FIXME
|
__description__ = "Piped API client"
|
||||||
__author__ = "SKevo"
|
__author__ = "SKevo"
|
||||||
__copyright__ = "Copyright (c) 2021, SKevo"
|
__copyright__ = "Copyright (c) 2021, SKevo"
|
||||||
__credits__ = ["SKevo"]
|
__credits__ = ["SKevo"]
|
||||||
|
@ -31,7 +31,7 @@ __doc__ = __readme__
|
||||||
|
|
||||||
|
|
||||||
setuptools.setup(
|
setuptools.setup(
|
||||||
name = 'python_project_template', # FIXME
|
name = 'piped_api',
|
||||||
packages = setuptools.find_packages(exclude=('tests',)),
|
packages = setuptools.find_packages(exclude=('tests',)),
|
||||||
|
|
||||||
long_description=__readme__,
|
long_description=__readme__,
|
||||||
|
@ -40,14 +40,14 @@ setuptools.setup(
|
||||||
version = __version__,
|
version = __version__,
|
||||||
license = __license__,
|
license = __license__,
|
||||||
description = __description__,
|
description = __description__,
|
||||||
keywords = [], # FIXME
|
keywords = ["piped", "api", "client"],
|
||||||
|
|
||||||
author = __author__,
|
author = __author__,
|
||||||
author_email = __email__,
|
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=[
|
classifiers=[
|
||||||
f'Development Status :: {__status__}',
|
f'Development Status :: {__status__}',
|
||||||
|
|
4
tests/__init__.py
Normal file
4
tests/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
from piped_api import PipedClient
|
||||||
|
|
||||||
|
|
||||||
|
CLIENT = PipedClient()
|
34
tests/test_comments.py
Normal file
34
tests/test_comments.py
Normal file
|
@ -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()
|
|
@ -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
|
|
Loading…
Reference in a new issue