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…
	
	Add table
		Add a link
		
	
		Reference in a new issue