from __future__ import annotations
from enum import Enum, IntEnum
BASE_URL = "https://catalog.he.u-tokyo.ac.jp/"
[docs]class Semester(Enum):
A1 = "A1"
A2 = "A2"
S1 = "S1"
S2 = "S2"
W = "W"
[docs]class Weekday(IntEnum):
Mon = 0
Tue = 1
Wed = 2
Thu = 3
Fri = 4
Sat = 5
Sun = 6
[docs]class Language(Enum):
"""Language of a course."""
Japanese = "ja"
English = "en"
JapaneseAndEnglish = "ja,en"
OtherLanguagesToo = "other"
OnlyOtherLanguages = "only_other"
Others = "others"
from typing import (
Coroutine,
Iterable,
Union,
TypeVar,
Callable,
Awaitable,
)
import asyncio
from functools import wraps
from datetime import datetime, timedelta
[docs]class RateLimitter:
_min_interval: timedelta
_last_called: datetime
def __init__(self, min_interval: Union[timedelta, int]):
"""Rate limitter.
Parameters
----------
min_interval : Union[timedelta, int]
Minimum interval between calls. If int, it is treated as seconds.
"""
if isinstance(min_interval, int):
min_interval = timedelta(seconds=min_interval)
self.min_interval = min_interval
self._last_called = datetime.min
@property
def last_called(self) -> datetime:
return self._last_called
@property
def next_call(self) -> datetime:
return self.last_called + self.min_interval
@property
def callable(self) -> bool:
return datetime.now() >= self.next_call
@property
def min_interval(self) -> timedelta:
return self._min_interval
@min_interval.setter
def min_interval(self, value: timedelta):
if value < timedelta(0):
raise ValueError("min_interval must be positive")
self._min_interval = value
[docs] async def wait(self) -> None:
while not self.callable:
await asyncio.sleep((self.next_call - datetime.now()).total_seconds())
self._last_called = datetime.now()
WrappedFnResult = TypeVar("WrappedFnResult")
WrappedFn = Callable[..., WrappedFnResult]
WrappedAwaitableFn = Callable[..., Awaitable[WrappedFnResult]]
[docs] def wraps(self, func: Union[WrappedFn, WrappedAwaitableFn]) -> WrappedAwaitableFn:
@wraps(func)
async def wrapper(*args, **kwargs) -> "RateLimitter.WrappedFnResult":
await self.wait()
if asyncio.iscoroutinefunction(func):
return await func(*args, **kwargs)
return func(*args, **kwargs)
return wrapper
async def __aenter__(self) -> "RateLimitter":
await self.wait()
return self
async def __aexit__(self, exc_type, exc_value, traceback) -> None:
pass
from typing import AsyncIterable
T = TypeVar("T")
[docs]def async_for_task(async_iterable: AsyncIterable[T]) -> Iterable[asyncio.Task[T]]:
iterator = type(async_iterable).__aiter__(async_iterable)
running = True
while running:
try:
coro = type(iterator).__anext__(iterator)
if asyncio.iscoroutine(coro):
yield asyncio.create_task(coro)
except StopAsyncIteration:
running = False
[docs]def async_iterable_to_iterable(
async_iterable: AsyncIterable[T],
) -> Iterable[Coroutine[None, None, T]]:
iterator = type(async_iterable).__aiter__(async_iterable)
running = True
while running:
try:
coro = type(iterator).__anext__(iterator)
if asyncio.iscoroutine(coro):
yield coro
except StopAsyncIteration:
running = False