Skip to content

Commit

Permalink
Merge pull request #4 from road-master/hotfix-radiko-program-id-is-no…
Browse files Browse the repository at this point in the history
…t-unique

Hotfix radiko program id is not unique
  • Loading branch information
road-master committed Mar 26, 2024
2 parents 890c699 + 336e9b1 commit ae7d7fb
Show file tree
Hide file tree
Showing 33 changed files with 102 additions and 54 deletions.
37 changes: 26 additions & 11 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,43 @@
"customizations": {
"vscode": {
"extensions": [
"GitHub.copilot",
"ms-python.python",
"charliermarsh.ruff",
"ms-python.flake8",
"ms-python.isort",
"ms-python.mypy-type-checker",
"ms-python.pylint",
"charliermarsh.ruff",
"wk-j.save-and-run",
"bungcip.better-toml",
"alexcvzz.vscode-sqlite",
"streetsidesoftware.code-spell-checker"
],
"settings": {
// Whether to display inlay hints for pytest fixture argument types.
// - Settings Reference for Python
// https://code.visualstudio.com/docs/python/settings-reference
"python.analysis.inlayHints.pytestParameters": true,
"python.formatting.provider": "black",
"python.linting.enabled": true,
"python.linting.lintOnSave": true,
"python.linting.banditEnabled": true,
"python.linting.flake8Enabled": true,
"python.linting.mypyEnabled": true,
"python.linting.pydocstyleEnabled": true,
// VS Code displays Pylint warning: import-error that not reported in Pylint by executing CLI.
// "python.linting.pylintEnabled": true,
// This setting forcibly generate __pycache__.
// "python.testing.pytestEnabled": true,
"[python]": {
"editor.formatOnSave": true
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.organizeImports": true
}
},
"emeraldwalk.runonsave": {
"commands": [
{
"match": ".*\\.py",
"cmd": "pipenv run autoflake --in-place ${file}"
},
{
"match": ".*\\.py",
"cmd": "pipenv run docformatter --in-place ${file}"
}
]
},
"cSpell.customDictionaries": {
"project-words": {
Expand Down
1 change: 1 addition & 0 deletions project-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mypy
nosemgrep
Picklable
pydocstyle
radiko
radikoapi
radikoplaylist
radikopodcast
Expand Down
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ ignored-classes = [

[tool.ruff]
line-length = 119
target-version = "py39"

[tool.ruff.lint]
select = [
"F", # Pyflakes
"E", # pycodestyle
Expand Down Expand Up @@ -221,4 +224,3 @@ ignore = [
"ANN102", # Missing type annotation for `cls` in classmethod
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed, These warnings are disabled by default
]
target-version = "py39"
1 change: 1 addition & 0 deletions radikopodcast/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Console script for radikopodcast."""

import asyncio
from pathlib import Path
import sys
Expand Down
1 change: 1 addition & 0 deletions radikopodcast/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Configuration."""

from dataclasses import dataclass, field

from yamldataclassconfig.config import YamlDataClassConfig
Expand Down
1 change: 1 addition & 0 deletions radikopodcast/database/database.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Database."""

from logging import getLogger
from typing import cast

Expand Down
12 changes: 7 additions & 5 deletions radikopodcast/database/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""This module implements SQLAlchemy database models."""

from __future__ import annotations

from abc import abstractmethod
Expand All @@ -8,7 +9,7 @@
from typing import cast, Generic, TYPE_CHECKING, TypeVar

from inflector import Inflector
from sqlalchemy import and_, Column, DATETIME, func, or_, String
from sqlalchemy import and_, Column, DATETIME, func, Integer, or_, String
from sqlalchemy.ext.declarative import declarative_base, declared_attr
from sqlalchemy.orm import Mapped, relationship
from sqlalchemy.sql.schema import ForeignKey, MetaData
Expand Down Expand Up @@ -79,8 +80,7 @@ def save_all(cls, models: Iterable[TypeVarModelInitByXml]) -> None:
class Station(ModelInitByXml[XmlParserStation]):
"""Station of radiko."""

# Reason: Design of table column.
id = Column(String(255), primary_key=True) # noqa: A003
id = Column(String(255), primary_key=True)
name = Column(String(255))
list_program: Mapped[list[Program]] = relationship("Program", backref="station")
transfer_target = Column(String(255))
Expand All @@ -101,7 +101,9 @@ class Program(ModelInitByXml[XmlParserProgram]):
"""Program of radiko."""

# Reason: Model. pylint: disable=too-many-instance-attributes
id = Column(String(255), primary_key=True) # noqa: A003
id = Column(Integer, autoincrement=True, primary_key=True)
# The id in the radiko API is not unique...
radiko_id = Column(String(255))
to = Column(DATETIME)
ft = Column(DATETIME)
title = Column(String(255))
Expand All @@ -112,7 +114,7 @@ class Program(ModelInitByXml[XmlParserProgram]):

def init(self, xml_parser: XmlParserProgram) -> None:
# Reason: "id" meets requirement of snake_case. pylint: disable=invalid-name
self.id = xml_parser.id
self.radiko_id = xml_parser.id
# Reason: Can't understand what "ft" points. pylint: disable=invalid-name
self.ft = xml_parser.ft
# Reason: "to" meets requirement of snake_case. pylint: disable=invalid-name
Expand Down
1 change: 1 addition & 0 deletions radikopodcast/database/program_downloader.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Download programs."""

from collections.abc import Generator
from datetime import date, datetime, timedelta
from logging import getLogger
Expand Down
1 change: 1 addition & 0 deletions radikopodcast/database/program_schedule.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Program schedule."""

from datetime import datetime
from typing import Optional

Expand Down
1 change: 1 addition & 0 deletions radikopodcast/database/session_manager.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""This module implements SQLAlchemy session life cycle to prevent forgetting close."""

from contextlib import AbstractContextManager
from types import TracebackType
from typing import Optional
Expand Down
1 change: 1 addition & 0 deletions radikopodcast/radiko_archiver.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Radiko Archiver."""

import asyncio
from logging import getLogger
import os
Expand Down
1 change: 1 addition & 0 deletions radikopodcast/radiko_datetime.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Datetime for radiko specification."""

from datetime import date, datetime, timedelta, timezone

JST = timezone(timedelta(hours=+9), "JST")
Expand Down
1 change: 1 addition & 0 deletions radikopodcast/radiko_podcast.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Main module."""

import asyncio
import logging
from logging import LogRecord
Expand Down
1 change: 1 addition & 0 deletions radikopodcast/radiko_stream_spec_factory.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Stream spec factory."""

from logging import getLogger
from pathlib import Path
from typing import TYPE_CHECKING
Expand Down
5 changes: 3 additions & 2 deletions radikopodcast/radikoapi/radiko_api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""radiko API."""
"""The radiko API."""

from logging import getLogger
from typing import TYPE_CHECKING

Expand All @@ -17,7 +18,7 @@


class RadikoApi:
"""radiko API."""
"""The radiko API."""

AREA_ID_DEFAULT = "JP13" # TOKYO

Expand Down
1 change: 1 addition & 0 deletions radikopodcast/radikoapi/requester.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""To unify error check and logging process."""

from http import HTTPStatus
from logging import getLogger

Expand Down
1 change: 1 addition & 0 deletions radikopodcast/radikoxml/xml_converter.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""XML converter."""

from abc import abstractmethod
from logging import getLogger
from typing import Generic, Optional, TYPE_CHECKING
Expand Down
5 changes: 3 additions & 2 deletions radikopodcast/radikoxml/xml_parser.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""XML parsers."""

from typing import Any, Callable, TYPE_CHECKING

from defusedxml import ElementTree
Expand Down Expand Up @@ -58,7 +59,7 @@ def __init__(

@property
# Reason: "id" meets requirement of snake_case. pylint: disable=invalid-name
def id(self) -> str: # noqa: A003
def id(self) -> str:
return self.element_tree_program.attrib["id"]

@property
Expand Down Expand Up @@ -110,7 +111,7 @@ def __init__(self, element_tree_station: "Element") -> None:

@property
# Reason: "id" meets requirement of snake_case. pylint: disable=invalid-name
def id(self) -> str: # noqa: A003
def id(self) -> str:
"""ID if find it, otherwise, raises error."""
element_id = self.element_tree_station.find("./id")
if element_id is None:
Expand Down
1 change: 1 addition & 0 deletions tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Execute 'invoke --list' for guidance on using Invoke
"""

from invoke import Collection
from invokelint import _clean, dist, lint, path, style, test

Expand Down
36 changes: 18 additions & 18 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"""Configuration of pytest."""

from dataclasses import dataclass, field
from datetime import date, datetime
import os
from pathlib import Path
from shutil import copyfile
from typing import Optional, TYPE_CHECKING
from unittest.mock import AsyncMock, MagicMock, Mock
from unittest.mock import AsyncMock, MagicMock

from asyncffmpeg.ffmpeg_coroutine_factory import FFmpegCoroutineFactory
from click.testing import CliRunner
Expand Down Expand Up @@ -88,16 +89,15 @@ def xml_station_name_lacked() -> str:
@pytest.fixture()
# Reason: To refer other fixture. pylint: disable=redefined-outer-name
def _mock_requests_program(requests_mock: "Mocker", xml_program: str) -> None:
# Reason: This is not requests but mock.
requests_mock.get( # nosec: B113
requests_mock.get(
"https://radiko.jp/v3/program/date/20210116/JP13.xml",
text=xml_program,
)


@dataclass
class GenreForXml:
id: str # Reason: To follow specification of radiko. # noqa: A003 pylint: disable=invalid-name
id: str # Reason: To follow specification of radiko. pylint: disable=invalid-name
name: str


Expand All @@ -109,7 +109,7 @@ class ProgramForXml:
"""

# Reason: Model. pylint: disable=too-many-instance-attributes
id: str # Reason: To follow specification of radiko. # noqa: A003 pylint: disable=invalid-name
id: str # Reason: To follow specification of radiko. pylint: disable=invalid-name
ft: datetime # Reason: To follow specification of radiko. pylint: disable=invalid-name
to: datetime # Reason: To follow specification of radiko. pylint: disable=invalid-name
title: str
Expand Down Expand Up @@ -137,7 +137,7 @@ def duration(self) -> str:

@dataclass
class StationForXml:
id: str # Reason: To follow specification of radiko. # noqa: A003 pylint: disable=invalid-name
id: str # Reason: To follow specification of radiko. pylint: disable=invalid-name
name: str
date: date
list_program: list[ProgramForXml]
Expand Down Expand Up @@ -188,8 +188,7 @@ def _mock_requests_program_week(resource_path_root: Path, requests_mock: "Mocker
]
for index, program in enumerate(list_xml_program):
program_date = 10 + index
# Reason: This is not requests but mock.
requests_mock.get( # nosec: B113
requests_mock.get(
f"https://radiko.jp/v3/program/date/202101{program_date}/JP13.xml",
text=program,
)
Expand Down Expand Up @@ -219,28 +218,29 @@ def mock_master_playlist_client(mocker: "MockFixture") -> MagicMock:
return mock_get # type: ignore[no-any-return]


class PicklableMock(Mock):
# Reason: AsyncMock's issue.
class PicklableAsyncMock(AsyncMock): # pylint: disable=too-many-ancestors
"""see: https://github.com/testing-cabal/mock/issues/139#issuecomment-122128815"""

def __reduce__(self) -> tuple[type[Mock], tuple[()]]:
return (Mock, ())
def __reduce__(self) -> tuple[type[AsyncMock], tuple[()]]:
return (AsyncMock, ())


@pytest.fixture()
def mock_ffmpeg_coroutine(mocker: "MockFixture") -> "Generator[PicklableMock, None, None]":
def mock_ffmpeg_coroutine(mocker: "MockFixture") -> "Generator[PicklableAsyncMock, None, None]":
"""Mock FFmpegCoroutine."""
mock = PicklableMock()
mock = PicklableAsyncMock()
# Reason: To init original mock. pylint: disable=attribute-defined-outside-init
mock.execute = AsyncMock()
mocker.patch.object(FFmpegCoroutineFactory, "create", mock)
mock.execute = PicklableAsyncMock()
mock_create = mocker.MagicMock(return_value=mock)
mocker.patch.object(FFmpegCoroutineFactory, "create", mock_create)
return mock


@pytest.fixture()
# Reason: To refer other fixture. pylint: disable=redefined-outer-name
def _mock_requests_station(requests_mock: "Mocker", xml_station: str) -> None:
# Reason: This is not requests but mock.
requests_mock.get( # nosec: B113
requests_mock.get(
"https://radiko.jp/v3/station/list/JP13.xml",
text=xml_station,
)
Expand Down Expand Up @@ -307,6 +307,6 @@ def _mock_all(
_mock_requests_station: None,
_mock_requests_program_week: None,
mock_master_playlist_client: MagicMock, # noqa: ARG001
mock_ffmpeg_coroutine: PicklableMock, # noqa: ARG001
mock_ffmpeg_coroutine: PicklableAsyncMock, # noqa: ARG001
) -> None:
"""Set of mocks for E2E test."""
1 change: 1 addition & 0 deletions tests/database/test_database.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for database.py."""

from typing import cast

import pytest
Expand Down
5 changes: 3 additions & 2 deletions tests/database/test_models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Test for models.py."""

from datetime import date
from typing import cast

Expand All @@ -22,15 +23,15 @@ def test_to_string_none(model_program: Program) -> None:
model_program.to = None
with pytest.raises(ValueError, match="None") as excinfo:
# Reason: Property has logic. pylint: disable=pointless-statement
model_program.to_string
model_program.to_string # noqa: B018
assert "None" in str(excinfo.value)

@staticmethod
def test_ft_string_none(model_program: Program) -> None:
model_program.ft = None
with pytest.raises(ValueError, match="None") as excinfo:
# Reason: Property has logic. pylint: disable=pointless-statement
model_program.ft_string
model_program.ft_string # noqa: B018
assert "None" in str(excinfo.value)

@pytest.mark.usefixtures("record_program")
Expand Down
1 change: 1 addition & 0 deletions tests/radikoapi/test_radiko_api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for radiko_api.py."""

from datetime import date
from typing import TYPE_CHECKING

Expand Down

0 comments on commit ae7d7fb

Please sign in to comment.