Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ FEATURE ] Logging #58

Merged
merged 3 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ print(doujin)

### Example 2: Extending with Custom Sources
```py
from enum import Enum
from typing import cast
from enma import Enma, SourcesEnum, Manganato, IMangaRepository

Expand All @@ -90,7 +89,6 @@ print(manga)

### Example 3: Downloading Chapters
```py
from enum import Enum
from enma import Enma, SourcesEnum, Manganato, IMangaRepository, default_downloader

enma = Enma()
Expand All @@ -111,6 +109,15 @@ manga.chapters[0].download(downloader=default_downloader)

```

## Logger Control
By default Enma sets logs as SILENT. But if you're needing to see what Enma outputs you can set log mode as NORMAL or DEBUG to deep logs.

```py
from enma import logger, LogMode

logger.mode = LogMode.NORMAL
```

## Retrieving `user-agent` and `cf_clearance` for NHentai

To retrieve the `user-agent` and `cf_clearance` for NHentai:
Expand Down
3 changes: 3 additions & 0 deletions enma/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sys
from enma.application.core.utils.logger import LogMode, logger
from enma.infra.entrypoints.lib import Enma, SourcesEnum, DefaultAvailableSources
from enma.infra.adapters.repositories.nhentai import CloudFlareConfig, NHentai, Sort
from enma.infra.adapters.repositories.manganato import Manganato
Expand All @@ -10,6 +11,8 @@
python_major = "3"
python_minor = "9"

logger.mode = LogMode.SILENT

try:
assert sys.version_info >= (int(python_major), int(python_minor))
except AssertionError:
Expand Down
Empty file removed enma/application/__init__.py
Empty file.
Empty file removed enma/application/core/__init__.py
Empty file.
Empty file.
Empty file.
77 changes: 77 additions & 0 deletions enma/application/core/utils/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from enum import Enum
import logging
from typing import Optional

class LogMode(Enum):
DEBUG = logging.DEBUG
SILENT = -1
NORMAL = logging.INFO

class CustomFormatter(logging.Formatter):
grey = "\x1b[38;21m"
green = "\x1b[32;21m"
yellow = "\x1b[33;21m"
red = "\x1b[31;21m"
bold_red = "\x1b[31;1m"
reset = "\x1b[0m"

_format = "%(name)s - [%(levelname)s]: %(message)s (%(filename)s:%(lineno)d)"

def __init__(self, template: Optional[str] = None):

if template is not None:
self._format = template

self.FORMATS = {
logging.DEBUG: self.grey + self._format + self.reset,
logging.INFO: self.green + self._format + self.reset,
logging.WARNING: self.yellow + self._format + self.reset,
logging.ERROR: self.red + self._format + self.reset,
logging.CRITICAL: self.bold_red + self._format + self.reset
}


def format(self, record):
log_fmt = self.FORMATS.get(record.levelno)
formatter = logging.Formatter(log_fmt, datefmt="%Y-%m-%d %H:%M:%S")
return formatter.format(record)

class Logger(logging.Logger):
def __init__(self, name: str) -> None:
super().__init__(name=name)
self._mode = LogMode.SILENT
self.create_handlers()

def create_handlers(self) -> None:
debug_formatter = CustomFormatter(template="%(asctime)s - %(name)s - [DEBUG] - %(message)s (in %(funcName)s at %(filename)s:%(lineno)d)")
info_formatter = CustomFormatter(template="%(name)s - [%(levelname)s]: %(message)s")

handler = logging.StreamHandler()
handler.setFormatter(debug_formatter)

self.addHandler(handler)

self._debug_formatter = debug_formatter
self._info_formatter = info_formatter

@property
def mode(self) -> LogMode:
return self._mode

@mode.setter
def mode(self, mode: LogMode) -> None:
if mode == LogMode.SILENT:
self.disabled = True
return

if mode == LogMode.DEBUG:
for handler in self.handlers:
handler.setFormatter(self._debug_formatter)
else:
for handler in self.handlers:
handler.setFormatter(self._info_formatter)

self.setLevel(mode.value)
self._mode = mode

logger = Logger('Enma')
Empty file.
2 changes: 2 additions & 0 deletions enma/application/use_cases/get_manga.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Union
from enma.application.core.interfaces.manga_repository import IMangaRepository
from enma.application.core.interfaces.use_case import DTO, IUseCase
from enma.application.core.utils.logger import logger
from enma.domain.entities.manga import Manga

@dataclass
Expand All @@ -19,6 +20,7 @@ def __init__(self, manga_repository: IMangaRepository):
self.__manga_repository = manga_repository

def execute(self, dto: DTO[GetMangaRequestDTO]) -> GetMangaResponseDTO:
logger.info(f'Fetching manga with identifier: {dto.data.identifier}.')
manga = self.__manga_repository.get(identifier=dto.data.identifier)

if manga is None: return GetMangaResponseDTO(found=False, manga=None)
Expand Down
2 changes: 2 additions & 0 deletions enma/application/use_cases/get_random.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Any
from enma.application.core.interfaces.manga_repository import IMangaRepository
from enma.application.core.interfaces.use_case import IUseCase
from enma.application.core.utils.logger import logger
from enma.domain.entities.manga import Manga

@dataclass
Expand All @@ -14,6 +15,7 @@ def __init__(self, manga_repository: IMangaRepository):
self.__manga_repository = manga_repository

def execute(self) -> RandomResponseDTO:
logger.info(f'Fetching random manga.')
result = self.__manga_repository.random()

return RandomResponseDTO(result=result)
2 changes: 2 additions & 0 deletions enma/application/use_cases/paginate.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from dataclasses import dataclass
from enma.application.core.interfaces.manga_repository import IMangaRepository
from enma.application.core.interfaces.use_case import DTO, IUseCase
from enma.application.core.utils.logger import logger
from enma.domain.entities.pagination import Pagination

@dataclass
Expand All @@ -17,6 +18,7 @@ def __init__(self, manga_repository: IMangaRepository):
self.__manga_repository = manga_repository

def execute(self, dto: DTO[PaginateRequestDTO]) -> PaginateResponseDTO:
logger.info(f'Retrieving page {dto.data.page}')
result = self.__manga_repository.paginate(page=dto.data.page)

return PaginateResponseDTO(result=result)
2 changes: 2 additions & 0 deletions enma/application/use_cases/search_manga.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from dataclasses import dataclass, field
from enma.application.core.interfaces.manga_repository import IMangaRepository
from enma.application.core.interfaces.use_case import DTO, IUseCase
from enma.application.core.utils.logger import logger
from enma.domain.entities.search_result import SearchResult

@dataclass
Expand All @@ -19,6 +20,7 @@ def __init__(self, manga_repository: IMangaRepository):
self.__manga_repository = manga_repository

def execute(self, dto: DTO[SearchMangaRequestDTO]) -> SearchMangaResponseDTO:
logger.info(f'Searching for {dto.data.query}.')
result = self.__manga_repository.search(query=dto.data.query,
page=dto.data.page,
**dto.data.extra)
Expand Down
Empty file removed enma/domain/__init__.py
Empty file.
Empty file removed enma/domain/entities/__init__.py
Empty file.
Empty file removed enma/infra/__init__.py
Empty file.
Empty file removed enma/infra/adapters/__init__.py
Empty file.
Empty file.
9 changes: 9 additions & 0 deletions enma/infra/adapters/repositories/manganato.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import requests

from enma.application.core.interfaces.manga_repository import IMangaRepository
from enma.application.core.utils.logger import logger
from enma.domain.entities.manga import Chapter, Genre, Image, Manga, Title
from enma.domain.entities.search_result import Pagination, SearchResult, Thumb

Expand All @@ -31,13 +32,16 @@ def __make_request(self,
headers = headers if headers is not None else {}
params = params if params is not None else {}

logger.debug(f'Fetching {url} with headers {headers} and params {params}')

return requests.get(url=urlparse(url).geturl(),
headers={**headers, 'Referer': 'https://chapmanganato.com/'},
params={**params})

def __create_title(self,
main_title: str,
alternative: str) -> Title:
logger.debug(f'Building manga title main: {main_title} and alternative: {alternative}')
jp, cn, *_ = alternative.split(';') if alternative.find(';') != -1 else alternative.split(',')
return Title(english=main_title.strip(),
japanese=jp.strip(),
Expand All @@ -50,8 +54,10 @@ def __find_chapets_list(self, html: BeautifulSoup) -> list[str]:

def __create_chapter(self, url: str) -> Chapter | None:
response = self.__make_request(url=url)
logger.debug(f'Fetching chapter {url}')

if response.status_code != 200:
logger.error(f'Could not fetch the chapter with url: {url}. status code: {response.status_code}')
return

chapter = Chapter(id=response.url.split('/')[-1])
Expand All @@ -66,6 +72,7 @@ def get(self, identifier: str) -> Manga | None:
response = self.__make_request(url=urljoin(self.__CHAPTER_BASE_URL, identifier))

if response.status_code != 200:
logger.error(f'Could not fetch the manga with identifier: {identifier}. status code: {response.status_code}')
return

soup = BeautifulSoup(response.text, 'html.parser')
Expand Down Expand Up @@ -100,6 +107,8 @@ def get(self, identifier: str) -> Manga | None:
author = author_cel.text.strip()
genres = genres_cel.text.replace('\n', '').split(' - ')

workers = 10
logger.debug(f'Initializing {workers} workers to fetch chapters of {identifier}.')
with ThreadPoolExecutor(max_workers=10) as executor:
chapters = executor.map(self.__create_chapter, self.__find_chapets_list(html=soup))
chapters = list(filter(lambda x: isinstance(x, Chapter), list(chapters)))
Expand Down
44 changes: 38 additions & 6 deletions enma/infra/adapters/repositories/nhentai.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from enma.application.core.handlers.error import (ExceedRetryCount,
NhentaiSourceWithoutConfig)
from enma.application.core.interfaces.manga_repository import IMangaRepository
from enma.application.core.utils.logger import logger
from enma.domain.entities.manga import (MIME, Chapter, Genre, Image, Manga,
Title)
from enma.domain.entities.search_result import Pagination, SearchResult, Thumb
Expand All @@ -40,7 +41,7 @@ class NHentai(IMangaRepository):
def __init__(self,
config: Optional[CloudFlareConfig] = None) -> None:
self.__config = config
self.__BASE_URL = 'https://nhentai.net/'
self.__BASE_URL = 'https://nhentai.net'
self.__API_URL = 'https://nhentai.net/api/'
self.__IMAGE_BASE_URL = 'https://i.nhentai.net/galleries/'
self.__AVATAR_URL = 'https://i5.nhentai.net/'
Expand All @@ -57,6 +58,8 @@ def __make_request(self,
headers = headers if headers is not None else {}
params = params if params is not None else {}

logger.debug(f'Fetching {url} with headers {headers} and params {params} the current config cf_clearance: {self.__config.cf_clearance}')

return requests.get(url=urlparse(url).geturl(),
headers={**headers, 'User-Agent': self.__config.user_agent},
params={**params},
Expand All @@ -70,21 +73,33 @@ def __make_page_uri(self,
media_id: str,
mime: MIME,
page_number: Optional[int] = None) -> str:
if type == 'cover': return urljoin(self.__TINY_IMAGE_BASE_URL, f'{media_id}/cover.{mime.value}')
if type == 'thumbnail': return urljoin(self.__TINY_IMAGE_BASE_URL, f'{media_id}/thumb.{mime.value}')
return urljoin(self.__IMAGE_BASE_URL, f'{media_id}/{page_number}.{mime.value}')

url = ''

if type == 'cover':
url = urljoin(self.__TINY_IMAGE_BASE_URL, f'{media_id}/cover.{mime.value}')
elif type == 'thumbnail':
url = urljoin(self.__TINY_IMAGE_BASE_URL, f'{media_id}/thumb.{mime.value}')
else:
url = urljoin(self.__IMAGE_BASE_URL, f'{media_id}/{page_number}.{mime.value}')

logger.debug(f'Built page uri for type {type} as {url}')

return url

def get(self, identifier: str) -> Manga | None:
response = self.__make_request(url=f'{self.__API_URL}/gallery/{identifier}')

if response.status_code != 200:
logger.error(f'Could not fetch {identifier} because nhentai\'s request ends up with {response.status_code} status code.')
return

doujin = response.json()

chapter = Chapter(id=0)

for index, page in enumerate(doujin.get('images').get('pages')):
logger.info(f'Building page {index} from chapter 0 from doujin {identifier}.')
page = Image(uri=self.__make_page_uri(type='page',
mime=MIME[doujin.get("images").get("thumbnail").get("t").upper()],
media_id=doujin.get('media_id'),
Expand Down Expand Up @@ -125,18 +140,26 @@ def search(self,
page: int,
sort: Sort = Sort.RECENT) -> SearchResult:

logger.debug(f'Searching into Nhentai with args query={query};page={page};sort={sort}')
request_response = self.__make_request(url=urljoin(self.__BASE_URL, 'search'),
params={'q': query,
'sort': sort if isinstance(sort, str) else sort.value,
'page': page})

if request_response.status_code != 200:
logger.error(f'Could not search by {query} because nhentai\'s request ends up with {request_response.status_code} status code.')

soup = BeautifulSoup(request_response.text, 'html.parser')

search_results_container = soup.find('div', {'class': 'container'})
logger.debug(f'Found successfully search result container using .class.container.')
pagination_container = soup.find('section', {'class': 'pagination'})
logger.debug(f'Found successfully pagination container using .class.pagination.')

last_page_a_tag = pagination_container.find('a', {'class': 'last'}) if pagination_container else None # type: ignore
logger.debug(f'Found last pagination container using .calss.last.')
total_pages = int(last_page_a_tag['href'].split('=')[-1]) if last_page_a_tag else 1 # type: ignore
logger.debug(f'Found last page number successfully splitting last pagination number href.')

if not search_results_container:
return SearchResult(query=query,
Expand All @@ -146,7 +169,8 @@ def search(self,
results=[])

search_results = search_results_container.find_all('div', {'class': 'gallery'}) # type: ignore

logger.debug(f'Found search results container using .class.gallery')

if not search_results:
return SearchResult(query=query,
total_pages=total_pages,
Expand Down Expand Up @@ -199,6 +223,7 @@ def paginate(self, page: int) -> Pagination:
params={'page': page})

if response.status_code != 200:
logger.error(f'Could not paginate to page {page} because nhentai\'s request ends up with {response.status_code} status code.')
return Pagination(page=page)

data = response.json()
Expand All @@ -222,15 +247,22 @@ def paginate(self, page: int) -> Pagination:
def random(self, retry=0) -> Manga:
response = self.__make_request(url=urljoin(self.__BASE_URL, 'random'))

if response.status_code != 200:
logger.error(f'Could not fetch a random manga because nhentai\'s request ends up with {response.status_code} status code.')

soup = BeautifulSoup(response.text, 'html.parser')

id = cast(Tag, soup.find('h3', id='gallery_id')).text.replace('#', '')

logger.debug(f'Got successfully random manga id {id} from "gallery_id.h3" tag.')
logger.debug(f'Using get method to Fetch manga with identifier: {id}')
doujin = self.get(identifier=id)
logger.debug(f'Got successfully random manga with id {id}.')

if doujin is None:
if retry == 4:
raise ExceedRetryCount('Could not fetch a random doujin.')

logger.warning('Could not fetch random manga. Retrying.')
return self.random(retry=retry+1)

return doujin
Empty file removed enma/infra/core/__init__.py
Empty file.
Empty file.
Empty file removed enma/infra/entrypoints/__init__.py
Empty file.
3 changes: 1 addition & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,4 @@ install_requires =
enma = *.txt, *.rst, *.md

[tool.setuptools_scm]
write_to = enma/_version.py
local_scheme = "dirty-tag"
write_to = enma/_version.py