コミットを比較
42 コミット
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
| 495c1381c7 | |||
| 1074f09b96 | |||
| 2b706f1247 | |||
| cb72b8dd99 | |||
| b2adf62090 | |||
| a3d9d0bfd7 | |||
| f44637d274 | |||
| f809e9faae | |||
| de8fd8634a | |||
| 88be511f6e | |||
| ea339f1ec9 | |||
| 06328a89b2 | |||
| 463e8bbec7 | |||
| baa75d68ba | |||
| 48e51f97d0 | |||
| c5204383ed | |||
| bf36d05ed3 | |||
| c9bd6fdfa7 | |||
| b2f5f81ca8 | |||
| 67b76e6dd4 | |||
| 6a5e6dfade | |||
| 6e99da7326 | |||
| 53ba658319 | |||
| 067c90890e | |||
| db14af1a73 | |||
| da17333a80 | |||
| ee971997ad | |||
| b448335851 | |||
| 9f810b23f0 | |||
| c91cf19926 | |||
| 6185788456 | |||
| 283b628053 | |||
| 05182b251f | |||
| 5eb3fb6037 | |||
| b6c041ddad | |||
| e23d919919 | |||
| 726b5dc47e | |||
| feb31dc10b | |||
| 980dd0ac60 | |||
| faf3b44745 | |||
| 988cf714f8 | |||
| 033652e1b7 |
@@ -0,0 +1 @@
|
||||
__pycache__
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import TypedDict
|
||||
|
||||
from eloquent import DatabaseManager, Model # type: ignore
|
||||
|
||||
CONFIG: dict[str, DbConfig] = { 'mysql': { 'driver': 'mysql',
|
||||
'host': 'localhost',
|
||||
'database': 'nizika_nico',
|
||||
'user': os.environ['MYSQL_USER'],
|
||||
'password': os.environ['MYSQL_PASS'],
|
||||
'prefix': '' } }
|
||||
|
||||
DB = DatabaseManager (CONFIG)
|
||||
Model.set_connection_resolver (DB)
|
||||
|
||||
|
||||
class DbConfig (TypedDict):
|
||||
driver: str
|
||||
host: str
|
||||
database: str
|
||||
user: str
|
||||
password: str
|
||||
prefix: str
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
# pylint: disable = missing-class-docstring
|
||||
# pylint: disable = missing-function-docstring
|
||||
# pylint: disable = missing-module-docstring
|
||||
# pylint: disable = unused-argument
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Generic, Type, TypeVar, overload
|
||||
from typing_extensions import Self
|
||||
|
||||
_ModelT = TypeVar ('_ModelT', bound = 'Model')
|
||||
|
||||
|
||||
class Connection:
|
||||
def select (self, query: str, bindings: dict[str, Any] | None = None) -> Any: ...
|
||||
|
||||
def insert (self, query: str, bindings: dict[str, Any] | None = None) -> int: ...
|
||||
|
||||
def update (self, query: str, bindings: dict[str, Any] | None = None) -> int: ...
|
||||
|
||||
def delete (self, query: str, bindings: dict[str, Any] | None = None) -> int: ...
|
||||
|
||||
def transaction (self, callback: Any) -> Any: ...
|
||||
|
||||
def begin_transaction (self) -> None: ...
|
||||
|
||||
def commit (self) -> None: ...
|
||||
|
||||
def rollback (self) -> None: ...
|
||||
|
||||
|
||||
class ConnectionResolver:
|
||||
def connection (self, name: str | None = None) -> Any: ...
|
||||
|
||||
def get_default_connection (self) -> str: ...
|
||||
|
||||
def set_default_connection (self, name: str) -> None: ...
|
||||
|
||||
|
||||
class DatabaseManager:
|
||||
connections: dict[str, Connection]
|
||||
|
||||
def __init__ (self, config: dict[str, Any]) -> None: ...
|
||||
|
||||
def connection (self, name: str | None = None) -> Connection: ...
|
||||
|
||||
def disconnect (self, name: str | None = None) -> None: ...
|
||||
|
||||
def reconnect (self, name: str | None = None) -> Connection: ...
|
||||
|
||||
def get_connections (self) -> dict[str, Connection]: ...
|
||||
|
||||
|
||||
class Model:
|
||||
id: int
|
||||
|
||||
_Model__exists: bool
|
||||
|
||||
def has_one (
|
||||
self,
|
||||
related_model: Type[_ModelT],
|
||||
foreign_key: str | None = None,
|
||||
) -> _ModelT: ...
|
||||
|
||||
def has_many (
|
||||
self,
|
||||
related_model: Type[_ModelT],
|
||||
foreign_key: str | None = None,
|
||||
) -> list[_ModelT]: ...
|
||||
|
||||
def belongs_to (
|
||||
self,
|
||||
related_model: Type[_ModelT],
|
||||
foreign_key: str | None = None,
|
||||
) -> _ModelT: ...
|
||||
|
||||
def belongs_to_many (
|
||||
self,
|
||||
related_model: Type[_ModelT],
|
||||
foreign_key: str | None = None,
|
||||
) -> list[_ModelT]: ...
|
||||
|
||||
def save (self) -> None: ...
|
||||
|
||||
def delete (self) -> None: ...
|
||||
|
||||
@classmethod
|
||||
def find (cls, id_: int) -> Self | None: ...
|
||||
|
||||
@classmethod
|
||||
def query (
|
||||
cls,
|
||||
) -> QueryBuilder[Self]: ...
|
||||
|
||||
@overload
|
||||
@classmethod
|
||||
def where (
|
||||
cls,
|
||||
field: str,
|
||||
operator: str,
|
||||
value: Any,
|
||||
) -> QueryBuilder[Self]: ...
|
||||
|
||||
@overload
|
||||
@classmethod
|
||||
def where (cls, field: str, value: Any) -> QueryBuilder[Self]: ...
|
||||
|
||||
@classmethod
|
||||
def where_not_in (
|
||||
cls,
|
||||
column: str,
|
||||
values: list[Any] | tuple
|
||||
) -> QueryBuilder[Self]: ...
|
||||
|
||||
@classmethod
|
||||
def where_not_null (cls, field: str) -> QueryBuilder[Self]: ...
|
||||
|
||||
@classmethod
|
||||
def max (cls, column: str) -> Any: ...
|
||||
|
||||
@classmethod
|
||||
def set_connection_resolver (cls, resolver: DatabaseManager) -> None: ...
|
||||
|
||||
|
||||
class QueryBuilder (Generic[_ModelT]):
|
||||
def first (self) -> _ModelT | None: ...
|
||||
|
||||
def get (self) -> list[_ModelT]: ...
|
||||
|
||||
@overload
|
||||
def where (
|
||||
self,
|
||||
field: str,
|
||||
operator: str,
|
||||
value: Any,
|
||||
) -> QueryBuilder[_ModelT]: ...
|
||||
|
||||
@overload
|
||||
def where (self, field: str, value: Any) -> QueryBuilder[_ModelT]: ...
|
||||
|
||||
def where_null (self, field: str) -> QueryBuilder[_ModelT]: ...
|
||||
|
||||
def max (self, column: str) -> Any: ...
|
||||
|
||||
def _load_relation (self, relation_name: str) -> QueryBuilder[_ModelT]: ...
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
# pylint: disable = missing-class-docstring
|
||||
# pylint: disable = missing-function-docstring
|
||||
|
||||
"""
|
||||
ぼざクリ DB の構成
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
from db.my_eloquent import Model
|
||||
|
||||
|
||||
class Comment (Model):
|
||||
# pylint: disable = too-many-instance-attributes
|
||||
|
||||
id: int
|
||||
video_id: int
|
||||
comment_no: int
|
||||
user_id: int
|
||||
content: str
|
||||
posted_at: datetime
|
||||
nico_count: int
|
||||
vpos_ms: int
|
||||
|
||||
__timestamps__ = False
|
||||
|
||||
@property
|
||||
def video (
|
||||
self,
|
||||
) -> Video:
|
||||
return self.belongs_to (Video)
|
||||
|
||||
@property
|
||||
def user (
|
||||
self,
|
||||
) -> User:
|
||||
return self.belongs_to (User)
|
||||
|
||||
def upsert (
|
||||
self,
|
||||
*args: str,
|
||||
) -> None:
|
||||
super ().upsert ('video_id', 'comment_no')
|
||||
|
||||
|
||||
class Tag (Model):
|
||||
id: int
|
||||
name: str
|
||||
|
||||
__timestamps__ = False
|
||||
|
||||
@property
|
||||
def video_tags (
|
||||
self,
|
||||
) -> list[VideoTag]:
|
||||
return self.has_many (VideoTag)
|
||||
|
||||
|
||||
class TrackedVideo (Model):
|
||||
id: int
|
||||
code: str
|
||||
|
||||
__timestamps__ = False
|
||||
|
||||
def upsert (
|
||||
self,
|
||||
*args: str,
|
||||
) -> None:
|
||||
super ().upsert ('code')
|
||||
|
||||
|
||||
class User (Model):
|
||||
id: int
|
||||
code: str
|
||||
|
||||
__timestamps__ = False
|
||||
|
||||
@property
|
||||
def comments (
|
||||
self,
|
||||
) -> list[Comment]:
|
||||
return self.has_many (Comment)
|
||||
|
||||
|
||||
class Video (Model):
|
||||
id: int
|
||||
code: str
|
||||
user_id: int | None
|
||||
title: str
|
||||
description: str
|
||||
uploaded_at: datetime
|
||||
deleted_at: datetime | None
|
||||
|
||||
__timestamps__ = False
|
||||
|
||||
@property
|
||||
def user (
|
||||
self,
|
||||
) -> User | None:
|
||||
if self.user_id is None:
|
||||
return None
|
||||
return self.belongs_to (User)
|
||||
|
||||
@property
|
||||
def video_histories (
|
||||
self,
|
||||
) -> list[VideoHistory]:
|
||||
return self.has_many (VideoHistory)
|
||||
|
||||
@property
|
||||
def video_tags (
|
||||
self,
|
||||
) -> list[VideoTag]:
|
||||
return self.has_many (VideoTag)
|
||||
|
||||
@property
|
||||
def comments (
|
||||
self,
|
||||
) -> list[Comment]:
|
||||
return self.has_many (Comment)
|
||||
|
||||
def upsert (
|
||||
self,
|
||||
*args: str,
|
||||
) -> None:
|
||||
super ().upsert ('code')
|
||||
|
||||
|
||||
class VideoHistory (Model):
|
||||
id: int
|
||||
video_id: int
|
||||
fetched_at: date
|
||||
views_count: int
|
||||
|
||||
__timestamps__ = False
|
||||
|
||||
@property
|
||||
def video (
|
||||
self,
|
||||
) -> Video:
|
||||
return self.belongs_to (Video)
|
||||
|
||||
def upsert (
|
||||
self,
|
||||
*args: str,
|
||||
) -> None:
|
||||
super ().upsert ('video_id', 'fetched_at')
|
||||
|
||||
|
||||
class VideoTag (Model):
|
||||
id: int
|
||||
video_id: int
|
||||
tag_id: int
|
||||
tagged_at: date
|
||||
untagged_at: date | None
|
||||
|
||||
__timestamps__ = False
|
||||
|
||||
@property
|
||||
def video (
|
||||
self,
|
||||
) -> Video:
|
||||
return self.belongs_to (Video)
|
||||
|
||||
@property
|
||||
def tag (
|
||||
self,
|
||||
) -> Tag:
|
||||
return self.belongs_to (Tag)
|
||||
|
||||
def upsert (
|
||||
self,
|
||||
*args: str,
|
||||
) -> None:
|
||||
super ().upsert ('video_id', 'tag_id')
|
||||
@@ -0,0 +1,50 @@
|
||||
# pylint: disable = missing-class-docstring
|
||||
# pylint: disable = missing-function-docstring
|
||||
|
||||
"""
|
||||
みてるぞ式魔改造(言ふほどか?)版 Eloquent
|
||||
"""
|
||||
|
||||
import eloquent
|
||||
|
||||
|
||||
class DatabaseManager (eloquent.DatabaseManager):
|
||||
pass
|
||||
|
||||
|
||||
class Model (eloquent.Model):
|
||||
id: int
|
||||
|
||||
def upsert (
|
||||
self,
|
||||
*args: str,
|
||||
) -> None:
|
||||
row = self._find_upsert_row (*args)
|
||||
if row is not None:
|
||||
self.id = row.id
|
||||
# pylint: disable = invalid-name
|
||||
# pylint: disable = attribute-defined-outside-init
|
||||
self._Model__exists = True
|
||||
self.save ()
|
||||
return
|
||||
|
||||
try:
|
||||
self.save ()
|
||||
except Exception:
|
||||
row = self._find_upsert_row (*args)
|
||||
if row is None:
|
||||
raise
|
||||
self.id = row.id
|
||||
# pylint: disable = invalid-name
|
||||
# pylint: disable = attribute-defined-outside-init
|
||||
self._Model__exists = True
|
||||
self.save ()
|
||||
|
||||
def _find_upsert_row (
|
||||
self,
|
||||
*args: str,
|
||||
):
|
||||
q = self.query ()
|
||||
for arg in args:
|
||||
q = q.where (arg, getattr (self, arg))
|
||||
return q.first ()
|
||||
シンボリックリンク
+1
@@ -0,0 +1 @@
|
||||
db/eloquent.pyi
|
||||
@@ -0,0 +1,55 @@
|
||||
# pylint: disable = missing-class-docstring
|
||||
# pylint: disable = missing-function-docstring
|
||||
|
||||
"""
|
||||
動画コードからコメントのリストを取得し,JSON 形式で出力する.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import TypedDict
|
||||
|
||||
from db.config import DB
|
||||
from db.models import Video
|
||||
|
||||
DB
|
||||
|
||||
|
||||
def main (
|
||||
video_code: str,
|
||||
) -> None:
|
||||
video = Video.where ('code', video_code).first ()
|
||||
if video:
|
||||
comments: list[CommentDict] = []
|
||||
for row in video.comments:
|
||||
comment: CommentDict = {
|
||||
'id': row.id,
|
||||
'video_id': row.video_id,
|
||||
'comment_no': row.comment_no,
|
||||
'user_id': row.user_id,
|
||||
'content': row.content,
|
||||
'posted_at': row.posted_at,
|
||||
'nico_count': row.nico_count,
|
||||
'vpos_ms': row.vpos_ms }
|
||||
comments.append (comment)
|
||||
print (json.dumps (comments, default = str))
|
||||
else:
|
||||
print ('[]')
|
||||
|
||||
|
||||
class CommentDict (TypedDict):
|
||||
id: int
|
||||
video_id: int
|
||||
comment_no: int
|
||||
user_id: int
|
||||
content: str
|
||||
posted_at: datetime
|
||||
nico_count: int
|
||||
vpos_ms: int
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main (sys.argv[1])
|
||||
@@ -0,0 +1,65 @@
|
||||
# pylint: disable = missing-class-docstring
|
||||
# pylint: disable = missing-function-docstring
|
||||
|
||||
"""
|
||||
動画履歴の情報を取得し,JSON 形式で出力する.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import date, datetime
|
||||
from typing import cast
|
||||
|
||||
from db.config import DB
|
||||
from db.models import Video, VideoHistory
|
||||
|
||||
DB
|
||||
|
||||
|
||||
def main (
|
||||
views_counts: list[int],
|
||||
base_date: date,
|
||||
) -> None:
|
||||
kiriban_list: list[tuple[int, str, str]] = []
|
||||
|
||||
latest_fetched_at = cast (date | None,
|
||||
(VideoHistory
|
||||
.where ('fetched_at', '<=', base_date)
|
||||
.max ('fetched_at')))
|
||||
if latest_fetched_at is None:
|
||||
print ('[]')
|
||||
return
|
||||
|
||||
for views_count in views_counts:
|
||||
targets = { vh.video.code for vh in (
|
||||
VideoHistory
|
||||
.where ('fetched_at', latest_fetched_at)
|
||||
.where ('views_count', '>=', views_count)
|
||||
.get ()) }
|
||||
|
||||
for code in targets:
|
||||
if code in [kiriban[1] for kiriban in kiriban_list]:
|
||||
continue
|
||||
|
||||
previous_views_count: int | None = (
|
||||
VideoHistory
|
||||
.where_has ('video', lambda q, code = code: q.where ('code', code))
|
||||
.where ('fetched_at', '<', latest_fetched_at)
|
||||
.max ('views_count'))
|
||||
if previous_views_count is None:
|
||||
previous_views_count = 0
|
||||
if previous_views_count >= views_count:
|
||||
continue
|
||||
|
||||
kiriban_list.append ((views_count, code,
|
||||
(cast (Video, Video.where ('code', code).first ())
|
||||
.uploaded_at)))
|
||||
|
||||
print (json.dumps (kiriban_list, default = str))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main (list (map (int, sys.argv[2:])),
|
||||
datetime.strptime (sys.argv[1], '%Y-%m-%d').date ())
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
# pylint: disable = missing-class-docstring
|
||||
# pylint: disable = missing-function-docstring
|
||||
|
||||
"""
|
||||
全動画の情報を取得し,JSON 形式で出力する.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import date, datetime
|
||||
from typing import TypedDict
|
||||
|
||||
from db.config import DB
|
||||
from db.models import Video
|
||||
|
||||
DB
|
||||
|
||||
|
||||
def main (
|
||||
) -> None:
|
||||
videos: list[VideoDict] = []
|
||||
for row in Video.all ():
|
||||
deleted_at = row.deleted_at.date () if row.deleted_at else None
|
||||
video: VideoDict = { 'id': row.id,
|
||||
'code': row.code,
|
||||
'user': getattr (row.user, 'code', None),
|
||||
'title': row.title,
|
||||
'description': row.description,
|
||||
'tags': [],
|
||||
'uploaded_at': row.uploaded_at,
|
||||
'deleted_at': deleted_at }
|
||||
for video_tag in row.video_tags:
|
||||
if video_tag.untagged_at is None:
|
||||
video['tags'].append (video_tag.tag.name)
|
||||
videos.append (video)
|
||||
|
||||
print (json.dumps (videos, default = str))
|
||||
|
||||
class VideoDict (TypedDict):
|
||||
id: int
|
||||
code: str
|
||||
user: str | None
|
||||
title: str
|
||||
description: str
|
||||
tags: list[str]
|
||||
uploaded_at: datetime
|
||||
deleted_at: date | None
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main ()
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE TABLE `nizika_nico`.`tracked_videos` (`id` BIGINT NOT NULL AUTO_INCREMENT , `code` VARCHAR(16) NOT NULL COMMENT '動画コード' , PRIMARY KEY (`id`)) ENGINE = InnoDB COMMENT = '追跡対象動画';
|
||||
ALTER TABLE `tracked_videos` ADD UNIQUE(`code`);
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE `videos` ADD `user_id` BIGINT NULL DEFAULT NULL COMMENT 'ユーザ Id.' AFTER `code`;
|
||||
ALTER TABLE `videos` ADD INDEX(`user_id`);
|
||||
ALTER TABLE `videos` ADD FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,19 @@
|
||||
import sys
|
||||
|
||||
from db.config import DB
|
||||
from db.models import TrackedVideo
|
||||
|
||||
DB
|
||||
|
||||
|
||||
def main (
|
||||
video_codes: list[str],
|
||||
) -> None:
|
||||
for code in video_codes:
|
||||
tv = TrackedVideo ()
|
||||
tv.code = code
|
||||
tv.upsert ()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main (sys.argv[1:])
|
||||
+282
-941
ファイル差分が大きすぎるため省略します
差分を読込み
新しい課題から参照
ユーザをブロックする