# -*- coding: utf-8 -*-
from setuptools import setup

packages = \
['faapi']

package_data = \
{'': ['*']}

install_requires = \
['beautifulsoup4>=4.10.0,<5.0.0',
 'cfscrape>=2.1.1,<3.0.0',
 'lxml>=4.8.0,<5.0.0',
 'python-dateutil>=2.8.1,<3.0.0',
 'requests>=2.27.1,<3.0.0']

setup_kwargs = {
    'name': 'faapi',
    'version': '3.4.3',
    'description': 'Python module to implement API-like functionality for the FurAffinity.net website.',
    'long_description': '<div align="center">\n\n<img alt="logo" width="400" src="https://raw.githubusercontent.com/FurryCoders/Logos/main/logos/faapi-transparent.png">\n\n# Fur Affinity API\n\nPython library to implement API-like functionality for the [Fur Affinity](https://furaffinity.net) website.\n\n[![](https://img.shields.io/pypi/v/faapi?logo=pypi)](https://pypi.org/project/faapi/)\n[![](https://img.shields.io/pypi/pyversions/faapi?logo=Python)](https://www.python.org)\n\n[![](https://img.shields.io/github/v/tag/FurryCoders/faapi?label=github&sort=date&logo=github&color=blue)](https://github.com/FurryCoders/faapi)\n[![](https://img.shields.io/github/issues/FurryCoders/faapi?logo=github&color=blue)](https://github.com/FurryCoders/FAAPI/issues)\n[![](https://img.shields.io/github/workflow/status/FurryCoders/FAAPI/Test?label=Test&logo=githubactions)](https://github.com/FurryCoders/FAAPI/actions/workflows/test.yml)\n\n</div>\n\n## Requirements\n\nPython 3.9+ is necessary to run this\nlibrary. [<img src="https://python-poetry.org/images/logo-origami.svg" height="12"> Poetry](https://python-poetry.org)\nis used for packaging and dependency management.\n\n## Usage\n\nThe API comprises a main class `FAAPI`, two submission classes `Submission` and `SubmissionPartial`, a journal\nclass `Journal`, and a user class `User`.\n\nOnce `FAAPI` is initialized, its methods can be used to crawl FA and return parsed objects.\n\n```python\nfrom requests.cookies import RequestsCookieJar\nimport faapi\nimport orjson\n\ncookies = RequestsCookieJar()\ncookies.set("a", "38565475-3421-3f21-7f63-3d341339737")\ncookies.set("b", "356f5962-5a60-0922-1c11-65003b703038")\n\napi = faapi.FAAPI(cookies)\nsub, sub_file = api.submission(12345678, get_file=True)\n\nprint(sub.id, sub.title, sub.author, f"{len(sub_file) / 1024:02f}KiB")\n\nwith open(f"{sub.id}.json", "wb") as f:\n    f.write(orjson.dumps(dict(sub)))\n\nwith open(sub.file_url.split("/")[-1], "wb") as f:\n    f.write(sub_file)\n\ngallery, _ = api.gallery("user_name", 1)\nwith open("user_name-gallery.json", "wb") as f:\n    f.write(orjson.dumps(list(map(dict, gallery))))\n```\n\n### robots.txt\n\nAt init, the `FAAPI` object downloads the [robots.txt](https://www.furaffinity.net/robots.txt) file from FA to determine\nthe `Crawl-delay` and `disallow` values set therein. If not set in the robots.txt file, a crawl delay value of 1 second\nis used.\n\nTo respect this value, the default behaviour of the `FAAPI` object is to wait when a get request is made if the last\nrequest was performed more recently then the crawl delay value.\n\nSee under [FAAPI](#faapi) for more details on this behaviour.\n\nFurthermore, any get operation that points to a disallowed path from robots.txt will raise an exception. This check\nshould not be circumvented, and the developer of this library does not take responsibility for violations of the TOS of\nFur Affinity.\n\n### Cookies\n\nTo access protected pages, cookies from an active session are needed. These cookies can be given to the FAAPI object as\na list of dictionaries - each containing a `name` and a `value` field -, or as a `http.cookiejar.CookieJar`\nobject (`requests.cookies.RequestsCookieJar` and other objects inheriting from `CookieJar` are also supported). The\ncookies list should look like the following example:\n\n```python\ncookies = [\n    {"name": "a", "value": "38565475-3421-3f21-7f63-3d3413397537"},\n    {"name": "b", "value": "356f5962-5a60-0922-1c11-65003b703038"},\n]\n```\n\n```python\nfrom requests.cookies import RequestsCookieJar\n\ncookies = RequestsCookieJar()\ncookies.set("a", "38565475-3421-3f21-7f63-3d3413397537")\ncookies.set("b", "356f5962-5a60-0922-1c11-65003b703038")\n```\n\nTo access session cookies, consult the manual of the browser used to log in.\n\n*Note:* it is important to not logout of the session the cookies belong to, otherwise they will no longer work.\n*Note:* as of 2021-12-05 only cookies `a` and `b` are needed.\n\n### User Agent\n\n`FAAPI` attaches a `User-Agent` header to every request. The user agent string is generated at startup in the following\nformat: `faapi/{library version} Python/{python version} {system name}/{system release}`.\n\n## Objects\n\n### FAAPI\n\nThis is the main object that handles all the calls to scrape pages and get submissions.\n\nIt holds 6 different fields:\n\n* `session: CloudflareScraper` `cfscrape` session used for get requests\n* `robots: urllib.robotparser.RobotFileParser` robots.txt handler\n* `user_agent: str` user agent used by the session (property, cannot be set)\n* `crawl_delay: float` crawl delay from robots.txt (property, cannot be set)\n* `last_get: float` time of last get (UNIX time)\n* `raise_for_unauthorized: bool = True` if set to `True`, raises an exception if a request is made and the resulting\n  page is not from a login session\n\n#### Init\n\n`__init__(cookies: List[dict] | CookieJar = None)`\n\nThe class init has a single optional argument `cookies` necessary to read logged-in-only pages. The cookies can be\nomitted, and the API will still be able to access public pages.\n\n*Note:* Cookies must be in the format mentioned above in [#Cookies](#cookies).\n\n#### Methods & Properties\n\n* `load_cookies(cookies: List[dict] | CookieJar)`<br>\n  Load new cookies and create a new session.<br>\n  *Note:* This method removes any cookies currently in use, to update/add single cookies access them from the session\n  object.\n* `handle_delay()`<br>\n  Handles the crawl delay as set in the robots.txt\n* `check_path(path: str, *, raise_for_disallowed: bool = False) -> bool`<br>\n  Checks whether a given path is allowed by the robots.txt. If `raise_for_disallowed` is set to `True`\n  a `DisallowedPath` exception is raised on non-allowed paths.\n* `connection_status -> bool`<br>\n  Returns the status of the connection.\n* `login_status -> bool`<br>\n  Returns the login status.\n* `get(path: str, **params) -> requests.Response`<br>\n  This returns a response object containing the result of the get operation on the given URL with the\n  optional `**params` added to it (url provided is considered as path from \'https://www.furaffinity.net/\').\n* `get_parsed(path: str, *, skip_page_check: bool = False, skip_auth_check: bool = False, **params) -> bs4.BeautifulSoup`<br>\n  Similar to `get()` but returns the parsed HTML from the normal get operation. If the GET request encountered an error,\n  an `HTTPError` exception is raised. If `skip_page_check` is set to `True`, the parsed page is not checked for errors (\n  e.g. non-existing submission). If `skip_auth_check` is set to `True`, the page is not checked for login status.\n* `me() -> Optional[User]`<br>\n  Returns the logged-in user as a `User` object if the cookies are from a login session.\n* `submission(submission_id: int, get_file: bool = False, *, chunk_size: int = None) -> Tuple[Submission, Optional[bytes]]`<br>\n  Given a submission ID, it returns a `Submission` object containing the various metadata of the submission itself and\n  a `bytes` object with the submission file if `get_file` is passed as `True`. The optional `chunk_size` argument is\n  used for the request; if left to `None` or set to 0 the download is performed directly without streaming.<br>\n  *Note:* the author `UserPartial` object of the submission does not contain the `join_date` field as it does not appear\n  on submission pages.\n* `submission_file(submission: Submission, *, chunk_size: int = None) -> bytes`<br>\n  Given a submission object, it downloads its file and returns it as a `bytes` object. The optional `chunk_size`\n  argument is used for the request; if left to `None` or set to 0 the download is performed directly without streaming.\n* `journal(journal_id: int) -> Journal`<br>\n  Given a journal ID, it returns a `Journal` object containing the various metadata of the journal.\n* `user(user: str) -> User`<br>\n  Given a username, it returns a `User` object containing information regarding the user.\n* `gallery(user: str, page: int = 1) -> Tuple[List[SubmissionPartial], int]`<br>\n  Returns the list of submissions found on a specific gallery page, and the number of the next page. The returned page\n  number is set to 0 if it is the last page.\n* `scraps(user: str, page: int = 1) -> -> Tuple[List[SubmissionPartial], int]`<br>\n  Returns the list of submissions found on a specific scraps page, and the number of the next page. The returned page\n  number is set to 0 if it is the last page.\n* `favorites(user: str, page: str = "") -> Tuple[List[SubmissionPartial], str]`<br>\n  Downloads a user\'s favorites page. Because of how favorites pages work on FA, the `page` argument (and the one\n  returned) are strings. If the favorites page is the last then an empty string is returned as next page. An empty page\n  value as argument is equivalent to page 1.<br>\n  *Note:* favorites page "numbers" do not follow any scheme and are only generated server-side.\n* `journals(user: str, page: int = 1) -> -> Tuple[List[Journal], int]`<br>\n  Returns the list of submissions found on a specific journals page, and the number of the next page. The returned page\n  number is set to 0 if it is the last page.\n* `search(q: str = "", page: int = 0, **params) -> Tuple[List[SubmissionPartial], int, int, int, int]`<br>\n  Parses FA search given the query (and optional other params) and returns the submissions found, and the next page\n  together with basic search statistics: the number of the first submission in the page (0-indexed), the number of the\n  last submission in the page (0-indexed), and the total number of submissions found in the search. For example if the\n  last three returned integers are 0, 47 and 437, then the page contains submissions 1 through 48 of a search that has\n  found a total of 437 submissions.<br>\n  *Note:* as of April 2021 the "/search" path is disallowed by Fur Affinity\'s robots.txt.\n* `watchlist_to(self, user: str) -> List[User]`<br>\n  Given a username, returns a list of `User` objects for each user that is watching the given user.\n* `watchlist_by(self, user: str) -> List[User]`<br>\n  Given a username, returns a list of `User` objects for each user that is watched by the given user.\n\n### Journal\n\nThis object contains information gathered when parsing a journals page, or a specific journal page. It contains the\nfollowing fields:\n\n* `id: int` journal id\n* `title: str` journal title\n* `date: datetime` upload date as a [`datetime` object](https://docs.python.org/3/library/datetime.html) (defaults to\n  timestamp 0)\n* `author: UserPartial` journal author (filled only if the journal is parsed from a `bs4.BeautifulSoup` page)\n* `content: str` journal content in HTML format\n* `mentions: List[str]` the users mentioned in the content (if they were mentioned as links, e.g. `:iconusername:`,\n  `@username`, etc.)\n* `user_icon_url: str` the URL to the user icon (cannot be parsed when downloading via `FAAPI.get_journals`)\n* `journal_item: Union[bs4.element.Tag, bs4.BeautifulSoup]` the journal tag/page used to parse the object fields\n\n`Journal` objects can be directly cast to a dict object or iterated through.\n\n#### Init\n\n`__init__(journal_item: Union[bs4.element.Tag, bs4.BeautifulSoup] = None)`\n\n`Journal` takes one optional parameters: a journal section tag from a journals page, or a parsed journal page. Parsing\nis then performed based on the class of the passed object.\n\n#### Methods\n\n* `url -> str`<br>\n  Property method that returns the Fur Affinity URL to the journal (`https://www.furaffinity.net/journal/{id}`).\n* `parse(journal_item: Union[bs4.element.Tag, bs4.BeautifulSoup] = None)`<br>\n  Parses the stored journal tag/page for information. If `journal_item` is passed, it overwrites the\n  existing `journal_item` value.\n\n### SubmissionPartial\n\nThis lightweight submission object is used to contain the information gathered when parsing gallery, scraps, favorites\nand search pages. It contains only the following fields:\n\n* `id: int` submission id\n* `title: str` submission title\n* `author: UserPartial` submission author (only the `name` field is filled)\n* `rating: str` submission rating [general, mature, adult]\n* `type: str` submission type [text, image, etc...]\n* `thumbnail_url: str` the URL to the submission thumbnail\n* `submission_figure: bs4.element.Tag` the figure tag used to parse the object fields\n\n`SubmissionPartial` objects can be directly cast to a dict object or iterated through.\n\n#### Init\n\n`__init__(submission_figure: bs4.element.Tag)`\n\n`SubmissionPartial` init needs a figure tag taken from a parsed page.\n\n#### Methods\n\n* `url -> str`<br>\n  Property method that returns the Fur Affinity URL to the submission (`https://www.furaffinity.net/view/{id}`).\n* `parse(submission_figure: bs4.element.Tag)`<br>\n  Parses the stored submission figure tag for information. If `submission_figure` is passed, it overwrites the\n  existing `submission_figure` value.\n\n### Submission\n\nThe main class that parses and holds submission metadata.\n\n* `id: int` submission id\n* `title: str` submission title\n* `author: UserPartial` submission author (only the `name`, `title`, and `user_icon_url` fields are filled)\n* `date: datetime` upload date as a [`datetime` object](https://docs.python.org/3/library/datetime.html) (defaults to\n  timestamp 0)\n* `tags: List[str]` tags list\n* `category: str` category\n* `species: str` species\n* `gender: str` gender\n* `rating: str` rating\n* `type: str` submission type (text, image, etc...)\n* `description: str` description in HTML format\n* `mentions: List[str]` the users mentioned in the description (if they were mentioned as links, e.g. `:iconusername:`,\n  `@username`, etc.)\n* `folder: str` the submission folder (gallery or scraps)\n* `file_url: str` the URL to the submission file\n* `thumbnail_url: str` the URL to the submission thumbnail\n* `submission_page: bs4.BeautifulSoup` the submission page used to parse the object fields\n* `prev: int` the ID of the previous submission (if any)\n* `next: int` the ID of the next submission (if any)\n\n`Submission` objects can be directly cast to a dict object and iterated through.\n\n#### Init\n\n`__init__(submission_page: bs4.BeautifulSoup = None)`\n\nTo initialise the object, an optional `bs4.BeautifulSoup` object is needed containing the parsed HTML of a submission\npage.\n\nIf no `submission_page` is passed then the object fields will remain at their default - empty - value.\n\n#### Methods\n\n* `url -> str`<br>\n  Property method that returns the Fur Affinity URL to the submission (`https://www.furaffinity.net/view/{id}`).\n* `parse(submission_page: bs4.BeautifulSoup = None)`<br>\n  Parses the stored submission page for metadata. If `submission_page` is passed, it overwrites the\n  existing `submission_page` value.\n\n### UserPartial\n\nA stripped-down class that holds basic user information. It is used to hold metadata gathered when parsing a submission,\njournal, gallery, scraps, etc.\n\n* `name: str` display name with capital letters and extra characters such as "_"\n* `status: str` user status (~, !, etc.)\n* `title: str` the user title as it appears on their userpage\n* `join_date: datetime` the date the user joined (defaults to timestamp 0)\n* `user_tag: bs4.element.Tag` the user element used to parse information (placeholder, `UserPartial` is filled\n  externally)\n\n#### Init\n\n`__init__(user_tag: bs4.element.Tag = None)`\n\nTo initialise the object, an optional `bs4.element.Tag` object is needed containing the user element from a user page or\nuser folder.\n\nIf no `user_tag` is passed then the object fields will remain at their default - empty - value.\n\n#### Methods\n\n* `name_url -> str`<br>\n  Property method that returns the URL-safe username\n* `url -> str`<br>\n  Property method that returns the Fur Affinity URL to the user (`https://www.furaffinity.net/user/{name_url}`).\n* `parse(user_page: bs4.BeautifulSoup = None)`<br>\n  Parses the stored user page for metadata. If `user_page` is passed, it overwrites the existing `user_page` value.\n\n### User\n\nA class that holds a user\'s main information.\n\n* `name: str` display name with capital letters and extra characters such as "_"\n* `status: str` user status (~, !, etc.)\n* `title: str` the user title as it appears on their userpage\n* `join_date: datetime` the date the user joined (defaults to timestamp 0)\n* `profile: str` profile text in HTML format\n* `stats: UserStats` user statistics sorted in a `namedtuple` (`views`, `submissions`, `favs`, `comments_earned`\n  , `comments_made`, `journals`, `watched_by`, `watching`)\n* `info: Dict[str, str]` profile information (e.g. "Accepting Trades", "Accepting Commissions", "Character Species",\n  etc.)\n* `contacts: Dict[str, str]` contact links (e.g. Twitter, Steam, etc.)\n* `user_icon_url: str` the URL to the user icon\n* `user_page: bs4.BeautifulSoup` the user page used to parse the object fields\n\n#### Init\n\n`__init__(user_page: bs4.BeautifulSoup = None)`\n\nTo initialise the object, an optional `bs4.BeautifulSoup` object is needed containing the parsed HTML of a submission\npage.\n\nIf no `user_page` is passed then the object fields will remain at their default - empty - value.\n\n#### Methods\n\n* `name_url -> str`<br>\n  Property method that returns the URL-safe username\n* `url -> str`<br>\n  Property method that returns the Fur Affinity URL to the user (`https://www.furaffinity.net/user/{name_url}`).\n* `parse(user_page: bs4.BeautifulSoup = None)`<br>\n  Parses the stored user page for metadata. If `user_page` is passed, it overwrites the existing `user_page` value.\n\n## Exceptions\n\nThe following are the exceptions explicitly raised by the FAAPI functions. The exceptions deriving from `ParsingError`\nare chosen depending on the content of the page. Because Fur Affinity doesn\'t use HTTP status codes besides 404, the\npage is checked against a static list of known error messages/page titles in order to determine the specific error to be\nused. If no match is found, then the `ServerError` and `NoticeMessage` exceptions are used instead. The actual error\nmessage parsed from the page is used as arguments for the exceptions, so that it can be analise further when caught.\n\n* `DisallowedPath(Exception)` The path is not allowed by the robots.txt.\n* `Unauthorized(Exception)` The user is not logged-in.\n* `ParsingError(Exception)` An error occurred while parsing the page.\n    * `NonePage(ParsingError)` The parsed page is None.\n    * `NotFound(ParsingError)` The resource could not be found.\n    * `NoTitle(ParsingError)` The parsed paged is missing a title.\n    * `DisabledAccount(ParsingError)` The resource belongs to a disabled account.\n    * `ServerError(ParsingError)` The page contains a server error notice.\n    * `NoticeMessage(ParsingError)` A notice of unknown type was found in the page.\n\n## Contributing\n\nAll contributions and suggestions are welcome!\n\nIf you have suggestions for fixes or improvements, you can open an issue with your idea, see [#Issues](#issues) for\ndetails.\n\n## Issues\n\nIf any problem is encountered during usage of the program, an issue can be opened\non [GitHub](https://github.com/FurryCoders/FAAPI/issues).\n\nIssues can also be used to suggest improvements and features.\n\nWhen opening an issue for a problem, please copy the error message and describe the operation in progress when the error\noccurred.\n',
    'author': 'Matteo Campinoti',
    'author_email': 'matteo.campinoti94@gmail.com',
    'maintainer': None,
    'maintainer_email': None,
    'url': 'https://gitlab.com/MatteoCampinoti94/FAAPI',
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
    'python_requires': '>=3.9,<4.0',
}


setup(**setup_kwargs)
