| @@ -10,15 +10,20 @@ import random | |||||
| import string | import string | ||||
| import time | import time | ||||
| from dataclasses import dataclass | from dataclasses import dataclass | ||||
| from datetime import datetime, timedelta | |||||
| from typing import Any, TypedDict, cast | |||||
| from datetime import date, datetime, timedelta | |||||
| from typing import Any, Self, Type, TypedDict, cast | |||||
| import mysql.connector | import mysql.connector | ||||
| import requests | import requests | ||||
| from mysql.connector.connection import MySQLConnectionAbstract | |||||
| # TODO: “何もしなぃ” を意味する None と区別可能にするため,NULL クラスを別途用意すること | |||||
| DbNull = None | |||||
| DbNullType = type (None) | |||||
| class DbNull: | |||||
| def __new__ ( | |||||
| cls, | |||||
| ): | |||||
| delattr (cls, '__init__') | |||||
| DbNullType = Type[DbNull] | |||||
| class VideoSearchParam (TypedDict): | class VideoSearchParam (TypedDict): | ||||
| @@ -55,11 +60,58 @@ class CommentResult (TypedDict): | |||||
| isMyPost: bool | isMyPost: bool | ||||
| class CommentRow (TypedDict): | |||||
| id: int | |||||
| video_id: int | |||||
| comment_no: int | |||||
| user_id: int | |||||
| content: str | |||||
| posted_at: datetime | |||||
| nico_count: int | |||||
| vpos_ms: int | None | |||||
| class TagRow (TypedDict): | |||||
| id: int | |||||
| name: str | |||||
| class UserRow (TypedDict): | |||||
| id: int | |||||
| code: str | |||||
| class VideoRow (TypedDict): | |||||
| id: int | |||||
| code: str | |||||
| title: str | |||||
| description: str | |||||
| uploaded_at: datetime | |||||
| deleted_at: datetime | None | |||||
| class VideoHistoryRow (TypedDict): | |||||
| id: int | |||||
| video_id: int | |||||
| fetched_at: date | |||||
| views_count: int | |||||
| class VideoTagRow (TypedDict): | |||||
| id: int | |||||
| video_id: int | |||||
| tag_id: int | |||||
| tagged_at: date | |||||
| untagged_at: date | None | |||||
| def main ( | def main ( | ||||
| ) -> None: | ) -> None: | ||||
| conn = mysql.connector.connect (user = os.environ['MYSQL_USER'], | conn = mysql.connector.connect (user = os.environ['MYSQL_USER'], | ||||
| password = os.environ['MYSQL_PASS'], | password = os.environ['MYSQL_PASS'], | ||||
| database = 'nizika_nico') | database = 'nizika_nico') | ||||
| if not isinstance (conn, MySQLConnectionAbstract): | |||||
| raise TypeError | |||||
| now = datetime.now () | now = datetime.now () | ||||
| @@ -248,7 +300,7 @@ def search_nico_by_tags ( | |||||
| class VideoDao: | class VideoDao: | ||||
| def __init__ ( | def __init__ ( | ||||
| self, | self, | ||||
| conn | |||||
| conn: MySQLConnectionAbstract, | |||||
| ): | ): | ||||
| self.conn = conn | self.conn = conn | ||||
| @@ -272,7 +324,7 @@ class VideoDao: | |||||
| id = %s | id = %s | ||||
| ORDER BY | ORDER BY | ||||
| id""", (video_id,)) | id""", (video_id,)) | ||||
| row = c.fetchone () | |||||
| row = cast (VideoRow | None, c.fetchone ()) | |||||
| if row is None: | if row is None: | ||||
| return None | return None | ||||
| return self._create_dto_from_row (row, with_relation_tables) | return self._create_dto_from_row (row, with_relation_tables) | ||||
| @@ -295,7 +347,7 @@ class VideoDao: | |||||
| ORDER BY | ORDER BY | ||||
| id""") | id""") | ||||
| videos: list[VideoDto] = [] | videos: list[VideoDto] = [] | ||||
| for row in c.fetchall (): | |||||
| for row in cast (list[VideoRow], c.fetchall ()): | |||||
| videos.append (self._create_dto_from_row (row, with_relation_tables)) | videos.append (self._create_dto_from_row (row, with_relation_tables)) | ||||
| return videos | return videos | ||||
| @@ -316,7 +368,7 @@ class VideoDao: | |||||
| WHERE | WHERE | ||||
| deleted_at IS NULL""") | deleted_at IS NULL""") | ||||
| videos: list[VideoDto] = [] | videos: list[VideoDto] = [] | ||||
| for row in c.fetchall (): | |||||
| for row in cast (list[VideoRow], c.fetchall ()): | |||||
| videos.append (self._create_dto_from_row (row, False)) | videos.append (self._create_dto_from_row (row, False)) | ||||
| return videos | return videos | ||||
| @@ -325,6 +377,13 @@ class VideoDao: | |||||
| video: VideoDto, | video: VideoDto, | ||||
| with_relation_tables: bool, | with_relation_tables: bool, | ||||
| ) -> None: | ) -> None: | ||||
| deleted_at: datetime | DbNullType | None = video.deleted_at | |||||
| if deleted_at is None: | |||||
| raise TypeError ('未実装') | |||||
| if deleted_at is DbNull: | |||||
| deleted_at = None | |||||
| deleted_at = cast (datetime | None, deleted_at) | |||||
| with self.conn.cursor (dictionary = True) as c: | with self.conn.cursor (dictionary = True) as c: | ||||
| c.execute (""" | c.execute (""" | ||||
| INSERT INTO | INSERT INTO | ||||
| @@ -350,7 +409,7 @@ class VideoDao: | |||||
| video.title, | video.title, | ||||
| video.description, | video.description, | ||||
| video.uploaded_at, | video.uploaded_at, | ||||
| video.deleted_at)) | |||||
| deleted_at)) | |||||
| video.id_ = c.lastrowid | video.id_ = c.lastrowid | ||||
| if with_relation_tables: | if with_relation_tables: | ||||
| if video.video_tags is not None: | if video.video_tags is not None: | ||||
| @@ -386,7 +445,7 @@ class VideoDao: | |||||
| def _create_dto_from_row ( | def _create_dto_from_row ( | ||||
| self, | self, | ||||
| row, | |||||
| row: VideoRow, | |||||
| with_relation_tables: bool, | with_relation_tables: bool, | ||||
| ) -> VideoDto: | ) -> VideoDto: | ||||
| video = VideoDto (id_ = row['id'], | video = VideoDto (id_ = row['id'], | ||||
| @@ -394,7 +453,7 @@ class VideoDao: | |||||
| title = row['title'], | title = row['title'], | ||||
| description = row['description'], | description = row['description'], | ||||
| uploaded_at = row['uploaded_at'], | uploaded_at = row['uploaded_at'], | ||||
| deleted_at = row['deleted_at']) | |||||
| deleted_at = row['deleted_at'] or DbNull) | |||||
| if with_relation_tables and video.id_ is not None: | if with_relation_tables and video.id_ is not None: | ||||
| video.video_tags = VideoTagDao (self.conn).fetch_by_video_id (video.id_, False) | video.video_tags = VideoTagDao (self.conn).fetch_by_video_id (video.id_, False) | ||||
| for i in range (len (video.video_tags)): | for i in range (len (video.video_tags)): | ||||
| @@ -424,7 +483,7 @@ class VideoDto: | |||||
| class VideoTagDao: | class VideoTagDao: | ||||
| def __init__ ( | def __init__ ( | ||||
| self, | self, | ||||
| conn, | |||||
| conn: MySQLConnectionAbstract, | |||||
| ): | ): | ||||
| self.conn = conn | self.conn = conn | ||||
| @@ -448,7 +507,7 @@ class VideoTagDao: | |||||
| ORDER BY | ORDER BY | ||||
| id""", (video_id,)) | id""", (video_id,)) | ||||
| video_tags: list[VideoTagDto] = [] | video_tags: list[VideoTagDto] = [] | ||||
| for row in c.fetchall (): | |||||
| for row in cast (list[VideoTagRow], c.fetchall ()): | |||||
| video_tags.append (self._create_dto_from_row (row, with_relation_tables)) | video_tags.append (self._create_dto_from_row (row, with_relation_tables)) | ||||
| return video_tags | return video_tags | ||||
| @@ -473,7 +532,7 @@ class VideoTagDao: | |||||
| ORDER BY | ORDER BY | ||||
| id""", (video_id,)) | id""", (video_id,)) | ||||
| video_tags: list[VideoTagDto] = [] | video_tags: list[VideoTagDto] = [] | ||||
| for row in c.fetchall (): | |||||
| for row in cast (list[VideoTagRow], c.fetchall ()): | |||||
| video_tags.append (self._create_dto_from_row (row, with_relation_tables)) | video_tags.append (self._create_dto_from_row (row, with_relation_tables)) | ||||
| return video_tags | return video_tags | ||||
| @@ -496,7 +555,7 @@ class VideoTagDao: | |||||
| WHERE | WHERE | ||||
| video_id = %s | video_id = %s | ||||
| AND tag_id = %s""", (video_id, tag_id)) | AND tag_id = %s""", (video_id, tag_id)) | ||||
| row = c.fetchone () | |||||
| row = cast (VideoTagRow, c.fetchone ()) | |||||
| if row is None: | if row is None: | ||||
| return None | return None | ||||
| return self._create_dto_from_row (row, with_relation_tables) | return self._create_dto_from_row (row, with_relation_tables) | ||||
| @@ -506,6 +565,13 @@ class VideoTagDao: | |||||
| video_tag: VideoTagDto, | video_tag: VideoTagDto, | ||||
| with_relation_tables: bool, | with_relation_tables: bool, | ||||
| ) -> None: | ) -> None: | ||||
| untagged_at: date | DbNullType | None = video_tag.untagged_at | |||||
| if untagged_at is None: | |||||
| raise TypeError ('未実装') | |||||
| if untagged_at is DbNull: | |||||
| untagged_at = None | |||||
| untagged_at = cast (date | None, untagged_at) | |||||
| with self.conn.cursor (dictionary = True) as c: | with self.conn.cursor (dictionary = True) as c: | ||||
| c.execute (""" | c.execute (""" | ||||
| INSERT INTO | INSERT INTO | ||||
| @@ -520,7 +586,7 @@ class VideoTagDao: | |||||
| %s, | %s, | ||||
| %s, | %s, | ||||
| %s)""", (video_tag.video_id, video_tag.tag_id, | %s)""", (video_tag.video_id, video_tag.tag_id, | ||||
| video_tag.tagged_at, video_tag.untagged_at)) | |||||
| video_tag.tagged_at, untagged_at)) | |||||
| video_tag.id_ = c.lastrowid | video_tag.id_ = c.lastrowid | ||||
| if with_relation_tables: | if with_relation_tables: | ||||
| if video_tag.video is not None: | if video_tag.video is not None: | ||||
| @@ -533,6 +599,13 @@ class VideoTagDao: | |||||
| video_tag: VideoTagDto, | video_tag: VideoTagDto, | ||||
| with_relation_tables: bool, | with_relation_tables: bool, | ||||
| ) -> None: | ) -> None: | ||||
| untagged_at: date | DbNullType | None = video_tag.untagged_at | |||||
| if untagged_at is None: | |||||
| raise TypeError ('未実装') | |||||
| if untagged_at is DbNull: | |||||
| untagged_at = None | |||||
| untagged_at = cast (date | None, untagged_at) | |||||
| with self.conn.cursor (dictionary = True) as c: | with self.conn.cursor (dictionary = True) as c: | ||||
| c.execute (""" | c.execute (""" | ||||
| INSERT INTO | INSERT INTO | ||||
| @@ -554,7 +627,7 @@ class VideoTagDao: | |||||
| untagged_at = VALUES(untagged_at)""", (video_tag.video_id, | untagged_at = VALUES(untagged_at)""", (video_tag.video_id, | ||||
| video_tag.tag_id, | video_tag.tag_id, | ||||
| video_tag.tagged_at, | video_tag.tagged_at, | ||||
| video_tag.untagged_at)) | |||||
| untagged_at)) | |||||
| video_tag.id_ = c.lastrowid | video_tag.id_ = c.lastrowid | ||||
| if with_relation_tables: | if with_relation_tables: | ||||
| if video_tag.video is not None: | if video_tag.video is not None: | ||||
| @@ -590,14 +663,14 @@ class VideoTagDao: | |||||
| def _create_dto_from_row ( | def _create_dto_from_row ( | ||||
| self, | self, | ||||
| row, | |||||
| row: VideoTagRow, | |||||
| with_relation_tables: bool, | with_relation_tables: bool, | ||||
| ) -> VideoTagDto: | ) -> VideoTagDto: | ||||
| video_tag = VideoTagDto (id_ = row['id'], | video_tag = VideoTagDto (id_ = row['id'], | ||||
| video_id = row['video_id'], | video_id = row['video_id'], | ||||
| tag_id = row['tag_id'], | tag_id = row['tag_id'], | ||||
| tagged_at = row['tagged_at'], | tagged_at = row['tagged_at'], | ||||
| untagged_at = row['untagged_at']) | |||||
| untagged_at = row['untagged_at'] or DbNull) | |||||
| if with_relation_tables: | if with_relation_tables: | ||||
| video_tag.video = VideoDao (self.conn).find (video_tag.video_id, True) | video_tag.video = VideoDao (self.conn).find (video_tag.video_id, True) | ||||
| video_tag.tag = TagDao (self.conn).find (video_tag.tag_id) | video_tag.tag = TagDao (self.conn).find (video_tag.tag_id) | ||||
| @@ -608,17 +681,17 @@ class VideoTagDao: | |||||
| class VideoTagDto: | class VideoTagDto: | ||||
| video_id: int | video_id: int | ||||
| tag_id: int | tag_id: int | ||||
| tagged_at: datetime | |||||
| id_: int | None = None | |||||
| untagged_at: datetime | DbNullType = DbNull | |||||
| video: VideoDto | None = None | |||||
| tag: TagDto | None = None | |||||
| tagged_at: date | |||||
| id_: int | None = None | |||||
| untagged_at: date | DbNullType = DbNull | |||||
| video: VideoDto | None = None | |||||
| tag: TagDto | None = None | |||||
| class TagDao: | class TagDao: | ||||
| def __init__ ( | def __init__ ( | ||||
| self, | self, | ||||
| conn, | |||||
| conn: MySQLConnectionAbstract, | |||||
| ): | ): | ||||
| self.conn = conn | self.conn = conn | ||||
| @@ -635,7 +708,7 @@ class TagDao: | |||||
| tags | tags | ||||
| WHERE | WHERE | ||||
| id = %s""", (tag_id,)) | id = %s""", (tag_id,)) | ||||
| row = c.fetchone () | |||||
| row = cast (TagRow | None, c.fetchone ()) | |||||
| if row is None: | if row is None: | ||||
| return None | return None | ||||
| return self._create_dto_from_row (row) | return self._create_dto_from_row (row) | ||||
| @@ -653,7 +726,7 @@ class TagDao: | |||||
| tags | tags | ||||
| WHERE | WHERE | ||||
| name = %s""", (tag_name,)) | name = %s""", (tag_name,)) | ||||
| row = c.fetchone () | |||||
| row = cast (TagRow | None, c.fetchone ()) | |||||
| if row is None: | if row is None: | ||||
| return None | return None | ||||
| return self._create_dto_from_row (row) | return self._create_dto_from_row (row) | ||||
| @@ -686,7 +759,7 @@ class TagDao: | |||||
| def _create_dto_from_row ( | def _create_dto_from_row ( | ||||
| self, | self, | ||||
| row, | |||||
| row: TagRow, | |||||
| ) -> TagDto: | ) -> TagDto: | ||||
| return TagDto (id_ = row['id'], | return TagDto (id_ = row['id'], | ||||
| name = row['name']) | name = row['name']) | ||||
| @@ -701,7 +774,7 @@ class TagDto: | |||||
| class VideoHistoryDao: | class VideoHistoryDao: | ||||
| def __init__ ( | def __init__ ( | ||||
| self, | self, | ||||
| conn, | |||||
| conn: MySQLConnectionAbstract, | |||||
| ): | ): | ||||
| self.conn = conn | self.conn = conn | ||||
| @@ -722,7 +795,7 @@ class VideoHistoryDao: | |||||
| WHERE | WHERE | ||||
| video_id = %s""", (video_id,)) | video_id = %s""", (video_id,)) | ||||
| video_histories: list[VideoHistoryDto] = [] | video_histories: list[VideoHistoryDto] = [] | ||||
| for row in c.fetchall (): | |||||
| for row in cast (list[VideoHistoryRow], c.fetchall ()): | |||||
| video_histories.append (self._create_dto_from_row (row, with_relation_tables)) | video_histories.append (self._create_dto_from_row (row, with_relation_tables)) | ||||
| return video_histories | return video_histories | ||||
| @@ -777,7 +850,7 @@ class VideoHistoryDao: | |||||
| def _create_dto_from_row ( | def _create_dto_from_row ( | ||||
| self, | self, | ||||
| row, | |||||
| row: VideoHistoryRow, | |||||
| with_relation_tables: bool, | with_relation_tables: bool, | ||||
| ) -> VideoHistoryDto: | ) -> VideoHistoryDto: | ||||
| video_history = VideoHistoryDto (id_ = row['id'], | video_history = VideoHistoryDto (id_ = row['id'], | ||||
| @@ -792,7 +865,7 @@ class VideoHistoryDao: | |||||
| @dataclass (slots = True) | @dataclass (slots = True) | ||||
| class VideoHistoryDto: | class VideoHistoryDto: | ||||
| video_id: int | video_id: int | ||||
| fetched_at: datetime | |||||
| fetched_at: date | |||||
| views_count: int | views_count: int | ||||
| id_: int | None = None | id_: int | None = None | ||||
| video: VideoDto | None = None | video: VideoDto | None = None | ||||
| @@ -801,7 +874,7 @@ class VideoHistoryDto: | |||||
| class CommentDao: | class CommentDao: | ||||
| def __init__ ( | def __init__ ( | ||||
| self, | self, | ||||
| conn, | |||||
| conn: MySQLConnectionAbstract, | |||||
| ): | ): | ||||
| self.conn = conn | self.conn = conn | ||||
| @@ -826,7 +899,7 @@ class CommentDao: | |||||
| WHERE | WHERE | ||||
| video_id = %s""", (video_id,)) | video_id = %s""", (video_id,)) | ||||
| comments: list[CommentDto] = [] | comments: list[CommentDto] = [] | ||||
| for row in c.fetchall (): | |||||
| for row in cast (list[CommentRow], c.fetchall ()): | |||||
| comments.append (self._create_dto_from_row (row, with_relation_tables)) | comments.append (self._create_dto_from_row (row, with_relation_tables)) | ||||
| return comments | return comments | ||||
| @@ -835,6 +908,13 @@ class CommentDao: | |||||
| comment: CommentDto, | comment: CommentDto, | ||||
| with_relation_tables: bool, | with_relation_tables: bool, | ||||
| ) -> None: | ) -> None: | ||||
| vpos_ms: int | DbNullType | None = comment.vpos_ms | |||||
| if vpos_ms is None: | |||||
| raise TypeError ('未実装') | |||||
| if vpos_ms is DbNull: | |||||
| vpos_ms = None | |||||
| vpos_ms = cast (int | None, vpos_ms) | |||||
| with self.conn.cursor (dictionary = True) as c: | with self.conn.cursor (dictionary = True) as c: | ||||
| c.execute (""" | c.execute (""" | ||||
| INSERT INTO | INSERT INTO | ||||
| @@ -868,7 +948,7 @@ class CommentDao: | |||||
| comment.content, | comment.content, | ||||
| comment.posted_at, | comment.posted_at, | ||||
| comment.nico_count, | comment.nico_count, | ||||
| comment.vpos_ms)) | |||||
| vpos_ms)) | |||||
| def upsert_all ( | def upsert_all ( | ||||
| self, | self, | ||||
| @@ -880,7 +960,7 @@ class CommentDao: | |||||
| def _create_dto_from_row ( | def _create_dto_from_row ( | ||||
| self, | self, | ||||
| row, | |||||
| row: CommentRow, | |||||
| with_relation_tables: bool, | with_relation_tables: bool, | ||||
| ) -> CommentDto: | ) -> CommentDto: | ||||
| comment = CommentDto (id_ = row['id'], | comment = CommentDto (id_ = row['id'], | ||||
| @@ -890,7 +970,7 @@ class CommentDao: | |||||
| content = row['content'], | content = row['content'], | ||||
| posted_at = row['posted_at'], | posted_at = row['posted_at'], | ||||
| nico_count = row['nico_count'], | nico_count = row['nico_count'], | ||||
| vpos_ms = row['vpos_ms']) | |||||
| vpos_ms = row['vpos_ms'] or DbNull) | |||||
| if with_relation_tables: | if with_relation_tables: | ||||
| comment.video = VideoDao (self.conn).find (comment.video_id, True) | comment.video = VideoDao (self.conn).find (comment.video_id, True) | ||||
| return comment | return comment | ||||
| @@ -913,7 +993,7 @@ class CommentDto: | |||||
| class UserDao: | class UserDao: | ||||
| def __init__ ( | def __init__ ( | ||||
| self, | self, | ||||
| conn, | |||||
| conn: MySQLConnectionAbstract, | |||||
| ): | ): | ||||
| self.conn = conn | self.conn = conn | ||||
| @@ -930,7 +1010,7 @@ class UserDao: | |||||
| users | users | ||||
| WHERE | WHERE | ||||
| code = %s""", (user_code,)) | code = %s""", (user_code,)) | ||||
| row = c.fetchone () | |||||
| row = cast (UserRow | None, c.fetchone ()) | |||||
| if row is None: | if row is None: | ||||
| return None | return None | ||||
| return self._create_dto_from_row (row) | return self._create_dto_from_row (row) | ||||
| @@ -949,7 +1029,7 @@ class UserDao: | |||||
| def _create_dto_from_row ( | def _create_dto_from_row ( | ||||
| self, | self, | ||||
| row, | |||||
| row: UserRow, | |||||
| ) -> UserDto: | ) -> UserDto: | ||||
| return UserDto (id_ = row['id'], | return UserDto (id_ = row['id'], | ||||
| code = row['code']) | code = row['code']) | ||||