Skip to content

Commit

Permalink
Add models for User and AO3Work
Browse files Browse the repository at this point in the history
  • Loading branch information
BruDriguezz committed Nov 5, 2024
1 parent 8cd27ea commit a24c0a0
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 27 deletions.
2 changes: 1 addition & 1 deletion src/ao3/_parser.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from bs4 import BeautifulSoup, NavigableString
from bs4 import BeautifulSoup


class AO3Soup(BeautifulSoup):
Expand Down
62 changes: 36 additions & 26 deletions src/ao3/_session.py → src/ao3/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

from requests import Session as ClientSession
from bs4 import BeautifulSoup

from ao3._parser import AO3Soup
from ao3.models.users import User

UserT = TypeVar(name="UserT") # Veeeery temporary. To be substituted by `User`!

Expand All @@ -17,38 +17,36 @@ def __init__(
{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"
}
) # TODO: This will become a centralized requester API soon-ish.

def _fetch_raw_data(
self,
target: str,
) -> str | None:
with self._session.get(
url=target,
) as result:
match result.status_code:
case 200:
return result.text
case _:
return None # TODO: Elaborate on this later...
)

def _soupify(
self,
text: str,
) -> BeautifulSoup:
return AO3Soup(
text, "lxml"
) # TODO: Implement custom soup for standardized parsing methods, soon... | Half-done? I still have to elaborate on it.
return AO3Soup(text, "lxml")

def fetch_page(
self,
target: str,
) -> BeautifulSoup: # TODO: This implementation is delayed until the custom `BeautifulSoup` is implemented.
result = self._fetch_raw_data(target=target)
if result:
return self._soupify(text=result)
) -> BeautifulSoup:
with self._session.get(target) as result:
return self._soupify(result.text)

# TODO: Consider if I'd like to implement an exception for this...
def post_to_page(
self,
target: str,
data: dict,
) -> ...:
with self._session.post(
url=target,
data=data,
) as result:
return self._soupify(result.text)

def __del__(
self,
) -> None:
self._session.close()


# TODO: Elaborate on this class later; it will require some careful thinking regarding how to manage user operations.
Expand All @@ -74,7 +72,7 @@ def user(
@property
def token(
self,
) -> str: # Returns ´str`, perhaps? Consider if we'd like a custom `Token` object.
) -> str:
return self._token

def login(
Expand All @@ -96,7 +94,19 @@ def login(
"x-csrf-token": self._token,
}
)
self._user = User(self._username, self, is_authenticated=True)
case _:
return False # TODO: Implement an exception for this... :)
return False # TODO: Implement an exception for this...

def refresh_token(self) -> None: ...
def refresh_token(self) -> None:
if self._token:
result = self.fetch_page(
"https://archiveofourown.org/users/{self._username}/"
)
self._token = result.fetch_token()
self._session.headers.update(
{
"x-requested-with": "XMLHttpRequest",
"x-csrf-token": self._token,
}
)
48 changes: 48 additions & 0 deletions src/ao3/models/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from __future__ import annotations

from typing import Type, Union, Optional
from ao3.models.works import AO3Work
from ao3.client import Session, UserSession


SessType = Union[Type[Session], Type[UserSession]]


class User:
def __init__(
self,
username: str,
id: Optional[int] = None,
user_id: Optional[int] = None,
session: Optional[SessType] = None,
is_authenticated: bool = False,
bio: Optional[str] = None,
joined_at: Optional[str] = None,
works: Optional[list[AO3Work]] = None,
history: Optional[list[AO3Work]] = None,
bookmarks: Optional[list[AO3Work]] = None,
pseudonyms: Optional[list[str]] = None,
series: Optional[list[str]] = None,
collections: Optional[list[str]] = None,
) -> None:
self.name = username
self.id = id
self.user_id = user_id
self.url = f"https://archiveofourown.org/users/{username}/"
self._session = session
self._is_authenticated = is_authenticated
self.bio = bio
self.joined_at = joined_at
self.works = works if works is not None else []
self.history = history if history is not None else []
self.bookmarks = bookmarks if bookmarks is not None else []
self.pseudonyms = pseudonyms if pseudonyms is not None else []
self.series = series if series is not None else []
self.collections = collections if collections is not None else []

def __repr__(self) -> str:
return f"<User(name={self.name}, id={self.id})>"

# TODO: Implement the rest of the methods.
# There's a fair bit of work to be done here, but I'll try tackling the
# methods definition first, and then move on to the implementation.
109 changes: 109 additions & 0 deletions src/ao3/models/works.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from __future__ import annotations

from typing import Optional
from ao3.models.users import User
from typing import Optional


class AO3Work:
"""
Represents a work on AO3 (Archive of Our Own).
Attributes
----------
title : str
The title of the work.
url : str
The URL of the work.
id : Optional[int]
The ID of the work.
author : User | str
The author of the work. Defaults to "Orphaned".
summary : Optional[str]
A summary of the work.
rating : Optional[str]
The rating of the work.
language : str
The language of the work. Defaults to "N/A".
chapters : str
The number of chapters in the work. Defaults to "0/?".
word_count : Optional[int]
The word count of the work. Defaults to 0.
tags : set[str]
The tags associated with the work.
warnings : set[str]
The warnings associated with the work.
categories : set[str]
The categories of the work.
fandoms : set[str]
The fandoms associated with the work.
relationships : set[tuple[str]]
The relationships in the work.
characters : set[str]
The characters in the work.
comments : int
The number of comments on the work.
hits : int
The number of hits on the work.
bookmarks : int
The number of bookmarks on the work.
published_at : Optional[str]
The publication date of the work. Defaults to "N/A".
updated_at : Optional[str]
The last update date of the work. Defaults to "N/A".
is_complete : bool
Whether the work is complete. Defaults to False.
"""

def __init__(
self,
title: str,
url: str,
id: Optional[int] = None,
author: User | str = "Orphaned",
summary: Optional[str] = None,
rating: Optional[str] = None,
language: str = "N/A",
chapters: str = "0/?",
word_count: Optional[int] = 0,
tags: Optional[set[str]] = None,
warnings: Optional[set[str]] = None,
categories: Optional[set[str]] = None,
fandoms: Optional[set[str]] = None,
relationships: Optional[set[tuple[str]]] = None,
characters: Optional[set[str]] = None,
comments: int = 0,
hits: int = 0,
bookmarks: int = 0,
published_at: Optional[str] = "N/A",
updated_at: Optional[str] = "N/A",
is_complete: bool = False,
) -> None:
self.title = title
self.url = url
self.id = id
self.author = author
self.summary = summary
self.rating = rating
self.language = language
self.chapters = chapters
self.word_count = word_count
self.tags = tags if tags is not None else set()
self.warnings = warnings if warnings is not None else set()
self.categories = categories if categories is not None else set()
self.fandoms = fandoms if fandoms is not None else set()
self.relationships = relationships if relationships is not None else set()
self.characters = characters if characters is not None else set()
self.comments = comments
self.hits = hits
self.bookmarks = bookmarks
self.published_at = published_at
self.updated_at = updated_at
self.is_complete = is_complete

def __repr__(self) -> str:
return f"<AO3Work(title={self.title}, id={self.id})>"

# TODO: Implement the rest of the methods.
# There's a fair bit of work to be done here, but I'll try tackling the
# methods definition first, and then move on to the implementation.

0 comments on commit a24c0a0

Please sign in to comment.