commit 7c965939d83e675ebd639f17a78fc5ac14a012f7 Author: agatha Date: Sat Sep 16 14:07:53 2023 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ba05c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.venv/ +.idea/ + +__pycache__/ + +*.py[cod] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..33719f3 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# pychan +Python package for interacting with the [4chan JSON API](https://github.com/4chan/4chan-API). + +## Usage +```python +from pychan.api import ChanAPI + +api = ChanAPI() + +# Get list of boards +boards = api.get_boards() + +# Get catalog pages for a board +pol_catalog = api.get_catalog('po') + +# Get high level stats for threads on a board +pol_threads = api.get_threads('po') + +# Get a thread by thread number +thread = api.get_thread('po', 570368) +``` + +## Notes +Object classes such as Board, Thread, and Post are in development and can be imported from [pychan/models.py](pychan/models.py). \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..2fdf899 --- /dev/null +++ b/main.py @@ -0,0 +1,13 @@ +from pychan.api import ChanAPI + + +def main(): + api = ChanAPI() + boards = api.get_boards() + for board in boards: + catalog = api.get_catalog(board['board']) + print(catalog) + + +if __name__ == '__main__': + main() diff --git a/pychan/__init__.py b/pychan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pychan/api.py b/pychan/api.py new file mode 100644 index 0000000..1ab692c --- /dev/null +++ b/pychan/api.py @@ -0,0 +1,75 @@ +""" +Module for interacting with the 4chan JSON API +""" +import json +import requests +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.util.retry import Retry + + +class InvalidApiResponse(Exception): + pass + + +class ApiRequestException(Exception): + pass + + +class ChanAPI: + """ + Wrapper class for interacting with the 4chan JSON API. + + Args: + headers (dict, optional): Dictionary containing customer headers to be sent with each request. + proxies (dict, optional): Dictionary containing proxy settings to be used for requests. + + Attributes: + base_url (str): The base URL of the API. + session (requests.Session): Persistent HTTP session. + """ + def __init__(self, headers=None, proxies=None): + self.base_url = 'https://a.4cdn.org' + self.session = requests.Session() + + retries = Retry(total=3, backoff_factor=1, status_forcelist=[429, 500, 502, 503, 504]) + self.session.mount('http://', HTTPAdapter(max_retries=retries)) + self.session.mount('https://', HTTPAdapter(max_retries=retries)) + + if headers: + self.session.headers.update(headers) + if proxies: + self.session.proxies.update(proxies) + + def __get(self, endpoint, params=None): + url = self.base_url + endpoint + + try: + response = self.session.get(url, params=params) + response.raise_for_status() + except requests.RequestException as e: + raise ApiRequestException(f'Failed to execute GET request: {str(e)}') + + try: + return response.json() + except json.JSONDecodeError: + raise InvalidApiResponse('Invalid JSON received') + + def get_boards(self): + endpoint = '/boards.json' + response = self.__get(endpoint) + return response['boards'] + + def get_catalog(self, board): + endpoint = f'/{board}/catalog.json' + response = self.__get(endpoint) + return response + + def get_threads(self, board): + endpoint = f'/{board}/threads.json' + response = self.__get(endpoint) + return response + + def get_thread(self, board, no): + endpoint = f'/{board}/thread/{no}.json' + response = self.__get(endpoint) + return response diff --git a/pychan/models.py b/pychan/models.py new file mode 100644 index 0000000..3aea71d --- /dev/null +++ b/pychan/models.py @@ -0,0 +1,46 @@ +import json +from abc import ABC + + +class ChanObject(ABC): + def __init__(self, data_dict=None): + self.data = {} + + if data_dict is not None: + self.load_json(json.dumps(data_dict)) + + def load_json(self, json_string): + self.data = json.loads(json_string) + + def to_json_string(self): + return json.dumps(self.data) + + def __getattr__(self, key): + return self.data[key] + + def __setattr__(self, key, value): + if key == 'data': + super().__setattr__(key, value) + else: + self.data[key] = value + + def __delattr__(self, key): + del self.data[key] + + def __repr__(self): + return self.to_json_string() + + +class Thread(ChanObject): + def __init__(self, data_dict=None): + super().__init__(data_dict) + + +class Post(ChanObject): + def __init__(self, data_dict=None): + super().__init__(data_dict) + + +class Board(ChanObject): + def __init__(self, data_dict=None): + super().__init__(data_dict) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..663bd1f --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests \ No newline at end of file