import functools
import operator
import pprint
import dateutil.parser
from wanikani_api import constants
from wanikani_api.exceptions import UnknownResourceException
class Resource:
def __init__(self, json_data, *args, **kwargs):
self.resource = json_data["object"]
self._raw = json_data
self.url = json_data["url"]
self.data_updated_at = parse8601(json_data["data_updated_at"])
# Some Resources do not have IDs.
self.id = (
None
if self.resource in constants.RESOURCES_WITHOUT_IDS
else json_data["id"]
)
self._resource = json_data["data"]
def raw_json(self):
return pprint.pformat(self._raw)
class Iterator:
def __init__(self, current_page, api_request, max_results=None, fetch_all=False):
self.current_page = current_page
self.api_request = api_request
self.max_results = max_results
self.yielded_count = 0
self.pages = [current_page]
if current_page:
self.per_page = current_page.per_page
if fetch_all:
self.fetch_all_pages()
def _keep_iterating(self):
return (
self.current_page is not None
and self.max_results
and self.yielded_count >= self.max_results
)
def fetch_next_page(self):
if self.current_page.next_page_url is not None:
self.pages.append(self.api_request(self.current_page.next_page_url))
self.current_page = self.pages[-1]
def fetch_all_pages(self):
while self.current_page.next_page_url is not None:
self.fetch_next_page()
def __iter__(self):
return iter([item for page in self.pages for item in page])
def __getitem__(self, item):
if isinstance(item, int):
return self.pages[item // self.per_page][item % self.per_page]
if isinstance(item, slice):
return [self[i] for i in range(*item.indices(len(self)))]
def __len__(self):
return functools.reduce(operator.add, [len(page) for page in self.pages])
class Page(Resource):
resource = "collection"
def __init__(self, json_data, *args, **kwargs):
super().__init__(json_data, *args, **kwargs)
self.client = kwargs.get("client")
self.next_page_url = json_data["pages"]["next_url"]
self.previous_page_url = json_data["pages"]["previous_url"]
self.total_count = json_data["total_count"]
self.per_page = json_data["pages"]["per_page"]
self.data = [factory(datum, client=self.client) for datum in json_data["data"]]
def __iter__(self):
return iter(self.data)
def __getitem__(self, item):
if isinstance(item, int):
return self.data[item]
elif isinstance(item, slice):
return [self.data[i] for i in range(*item.indices(len(self)))]
def __len__(self):
return len(self.data)
[docs]class Subjectable:
"""
A Mixin allowing a model to quickly fetch related subjects.
Any resource which inherits Subjectable must have either `subject_id` or `subject_ids` as an attribute.
"""
def __init__(self, *args, **kwargs):
if "client" not in kwargs:
raise ValueError("Subjectable models expect an instance of Client!")
self.client = kwargs.get("client")
self._subjects = None
self._subject = None
@property
def subject(self):
if self._subject:
return self._subject
elif hasattr(self, "subject_id"):
self._subject = self.client.subject(self.subject_id)
return self._subject
else:
raise AttributeError("no attribute named subject!")
@property
def subjects(self):
if self._subjects:
return self._subjects
elif hasattr(self, "subject_ids"):
self._subjects = self.client.subjects(ids=self.subject_ids)
return self._subjects
else:
raise AttributeError("no attribute named subjects!")
class Preferences():
def __init__(self, preferences_json):
self.default_voice_actor_id = preferences_json.get("default_voice_actor_id")
self.lessons_batch_size= preferences_json["lessons_batch_size"]
self.lessons_autoplay_audio = preferences_json["lessons_autoplay_audio"]
self.reviews_autoplay_audio = preferences_json["reviews_autoplay_audio"]
self.lessons_presentation_order = preferences_json["lessons_presentation_order"]
self.reviews_display_srs_indicator = preferences_json["reviews_display_srs_indicator"]
[docs]class Subject(Resource):
"""
This is the base Subject for Wanikani. This contains information common to Kanji, Vocabulary, and Radicals.
"""
def __init__(self, json_data, *args, **kwargs):
super().__init__(json_data, *args, **kwargs)
resource_data = json_data["data"]
self.level = resource_data["level"] #: The level of the subject.
self.created_at = parse8601(
resource_data["created_at"]
) #: The date at which the Subject was created originally on Wanikani.
self.characters = resource_data[
"characters"
] #: The actual japanese kanji/radical symbol such as 女
self.meanings = [
Meaning(meaning_json) for meaning_json in resource_data["meanings"]
] #: A list of :class:`.models.Meaning` for this subject.
self.document_url = resource_data[
"document_url"
] #: The direct URL where the subject can be found on Wanikani
self.hidden_at = resource_data[
"hidden_at"
] #: When Wanikani removes a subject, they seem to instead set it to hidden, for backwards compatibilty with clients.
self.auxiliary_meanings = [
AuxiliaryMeaning(auxiliary_meaning_json) for auxiliary_meaning_json in resource_data["auxiliary_meanings"]
]
def __str__(self) -> str:
return f"{['['+meaning.meaning+']' if meaning.primary else meaning.meaning for meaning in self.meanings]}:{[character for character in self.characters] if self.characters else 'UNAVAILABLE'}"
[docs]class Radical(Subject):
"""
A model for the Radical object.
"""
resource = "radical"
def __init__(self, json_data, *args, **kwargs):
super().__init__(json_data, *args, **kwargs)
self.character_images = (
self._resource["character_images"]
if "character_images" in self._resource.keys()
else None
) #: A list of dictionaries, each containing a bunch of information related to a single character image.
self.amalgamation_subject_ids = self._resource[
"amalgamation_subject_ids"
] #: IDs for various other :class:`.models.Subject` for which this radical is a component.
def __str__(self) -> str:
return f"Radical: {[meaning.meaning for meaning in self.meanings]}:{[character for character in self.characters] if self.characters else 'UNAVAILABLE'}"
[docs]class Vocabulary(Subject):
"""
A model for the Vocabulary Resource
"""
resource = "vocabulary"
def __init__(self, json_data, *args, **kwargs):
super().__init__(json_data, *args, **kwargs)
self.parts_of_speech = self._resource[
"parts_of_speech"
] #: A list of strings, each of which is a part of speech.
self.component_subject_ids = self._resource[
"component_subject_ids"
] #: List of IDs for :class"`.models.Kanji` which make up this vocabulary.
self.readings = [
Reading(reading_json) for reading_json in self._resource["readings"]
] #: A list of :class:`.models.Reading` related to this Vocabulary.
def __str__(self):
return f"Vocabulary: {super(Vocabulary, self).__str__()}"
[docs]class Kanji(Subject):
"""
A model for the Kanji Resource
"""
resource = "kanji"
def __init__(self, json_data, *args, **kwargs):
super().__init__(json_data, *args, **kwargs)
self.amalgamation_subject_ids = self._resource[
"amalgamation_subject_ids"
] #: A list of IDs for the related :class:`.models.Vocabulary` which this Kanji is a component in.
self.component_subject_ids = self._resource[
"component_subject_ids"
] #: A list of IDs for the related :class:`.models.Radical` which combine to make this kanji
self.readings = [
Reading(reading_json) for reading_json in self._resource["readings"]
] #: A list of :class:`.models.Reading` related to this Vocabulary.
def __str__(self):
return f"Kanji: {super(Kanji, self).__str__()}"
[docs]class AuxiliaryMeaning:
"""
Simple data class for handling auxiliary meanings
"""
def __init__(self, auxiliary_meaning_json):
self.meaning = auxiliary_meaning_json["meaning"]
self.type = auxiliary_meaning_json["type"]
def __str__(self) -> str:
return f"{self.meaning}({type})"
class Subscription:
def __init__(self, subscription_json):
self.active = subscription_json["active"]
self.type = subscription_json["type"]
self.max_level_granted = subscription_json["max_level_granted"]
self.period_ends_at = parse8601(subscription_json["period_ends_at"])
def __str__(self) -> str:
return f"{'active' if self.active else 'inactive'}:{self.type}:{self.max_level_granted}:{self.period_ends_at}"
[docs]class Meaning:
"""
Simple class holding information about a given meaning of a vocabulary/Kanji
"""
def __init__(self, meaning_json):
self.meaning = meaning_json["meaning"] #: The english meaning of a Subject.
self.primary = meaning_json[
"primary"
] #: Wether or not the meaning is considered to be the main one.
self.accepted_answer = meaning_json[
"accepted_answer"
] #: Whether or not this answer is accepted during reviews in Wanikani.
def __str__(self) -> str:
return self.meaning
[docs]class Reading:
"""
Simple class holding information about a given reading of a vocabulary/kanji
"""
def __init__(self, meaning_json):
#: the actual かな for the reading.
self.reading = meaning_json["reading"]
self.primary = meaning_json["primary"] #: Whether this is the primary reading.
self.accepted_answer = meaning_json[
"accepted_answer"
] #: Whether this answer is accepted as correct by Wanikani during review.
[docs]class Assignment(Resource, Subjectable):
"""
Simple class holding information about Assignmetns.
"""
resource = "assignment"
def __init__(self, json_data, *args, **kwargs):
super().__init__(json_data, *args, **kwargs)
Subjectable.__init__(self, *args, **kwargs)
self.created_at = parse8601(self._resource["created_at"])
self.subject_id = self._resource["subject_id"]
self.subject_type = self._resource["subject_type"]
self.srs_stage = self._resource["srs_stage"]
self.unlocked_at = parse8601(self._resource["unlocked_at"])
self.started_at = parse8601(self._resource["started_at"])
self.passed_at = parse8601(self._resource["passed_at"])
self.burned_at = parse8601(self._resource["burned_at"])
self.available_at = parse8601(self._resource["available_at"])
self.resurrected_at_at = parse8601(self._resource["resurrected_at"])
self.hidden = self._resource["hidden"]
[docs]class Reset(Resource):
"""
Simple model holding resource information
"""
resource = "reset"
def __init__(self, json_data, *args, **kwargs):
super().__init__(json_data, *args, **kwargs)
self.created_at = parse8601(self._resource["created_at"])
self.original_level = self._resource["original_level"]
self.target_level = self._resource["target_level"]
self.confirmed_at = parse8601(self._resource["confirmed_at"])
[docs]class ReviewStatistic(Resource):
"""
Simple model holding ReviewStatistic Information
"""
resource = "review_statistic"
def __init__(self, json_data, *args, **kwargs):
super().__init__(json_data, *args, **kwargs)
self.created_at = parse8601(self._resource["created_at"])
self.subject_id = self._resource["subject_id"]
self.subject_type = self._resource["subject_type"]
self.meaning_correct = self._resource["meaning_correct"]
self.meaning_incorrect = self._resource["meaning_incorrect"]
self.meaning_max_streak = self._resource["meaning_max_streak"]
self.meaning_current_streak = self._resource["meaning_current_streak"]
self.reading_correct = self._resource["reading_correct"]
self.reading_incorrect = self._resource["reading_incorrect"]
self.reading_max_streak = self._resource["reading_max_streak"]
self.reading_current_streak = self._resource["reading_current_streak"]
self.percentage_correct = self._resource["percentage_correct"]
self.hidden = self._resource["hidden"]
[docs]class StudyMaterial(Resource, Subjectable):
"""
Simple model holding information about Study Materials
"""
resource = "study_material"
def __init__(self, json_data, *args, **kwargs):
super().__init__(json_data, *args, **kwargs)
Subjectable.__init__(self, *args, **kwargs)
self.created_at = parse8601(self._resource["created_at"])
self.subject_id = self._resource["subject_id"]
self.subject_type = self._resource["subject_type"]
self.meaning_note = self._resource["meaning_note"]
self.reading_note = self._resource["reading_note"]
self.meaning_synonyms = self._resource["meaning_synonyms"]
self.hidden = self._resource["hidden"]
class Lessons(object):
def __init__(self, json_data, *args, **kwargs):
self.subject_ids = json_data["subject_ids"]
self.available_at = parse8601(json_data["available_at"])
[docs]class UpcomingReview(Subjectable):
def __init__(self, json_data, *args, **kwargs):
super().__init__(json_data, *args, **kwargs)
Subjectable.__init__(self, *args, **kwargs)
self.subject_ids = json_data["subject_ids"]
self.available_at = parse8601(json_data["available_at"])
class Summary(Resource):
resource = "report"
def __init__(self, json_data, *args, **kwargs):
super().__init__(json_data, *args, **kwargs)
self.client = kwargs.get("client")
# Note that there is only ever one lesson object, as per this forum thread https://community.wanikani.com/t/api-v2-alpha-documentation/18987
self.lessons = Lessons(self._resource["lessons"][0])
self.next_reviews_at = self._resource["next_reviews_at"]
self.reviews = [
UpcomingReview(review_json, client=self.client)
for review_json in self._resource["reviews"]
]
[docs]class Review(Resource, Subjectable):
resource = "review"
def __init__(self, json_data, *args, **kwargs):
super().__init__(json_data, *args, **kwargs)
Subjectable.__init__(self, *args, **kwargs)
self.created_at = parse8601(self._resource["created_at"])
self.assignment_id = self._resource["assignment_id"]
self.subject_id = self._resource["subject_id"]
self.starting_srs_stage = self._resource["starting_srs_stage"]
self.ending_srs_stage = self._resource["ending_srs_stage"]
self.incorrect_meaning_answers = self._resource["incorrect_meaning_answers"]
self.incorrect_reading_answers = self._resource["incorrect_reading_answers"]
class LevelProgression(Resource):
resource = "level_progression"
def __init__(self, json_data, *args, **kwargs):
super().__init__(json_data, *args, **kwargs)
self.created_at = parse8601(self._resource["created_at"])
self.level = self._resource["level"]
self.unlocked_at = parse8601(self._resource["unlocked_at"])
self.started_at = parse8601(self._resource["started_at"])
self.passed_at = parse8601(self._resource["passed_at"])
self.completed_at = parse8601(self._resource["completed_at"])
def parse8601(time_field):
if time_field:
return dateutil.parser.parse(time_field)
else:
return None
resources = {
UserInformation.resource: UserInformation,
Assignment.resource: Assignment,
Review.resource: Review,
ReviewStatistic.resource: ReviewStatistic,
LevelProgression.resource: LevelProgression,
StudyMaterial.resource: StudyMaterial,
Reset.resource: Reset,
Kanji.resource: Kanji,
Vocabulary.resource: Vocabulary,
Radical.resource: Radical,
Summary.resource: Summary,
Page.resource: Page,
}
def factory(resource_json, *args, **kwargs):
try:
return resources[resource_json["object"]](resource_json, *args, **kwargs)
except KeyError:
raise UnknownResourceException(
"We have no clue how to handle resource of type: {}".format(
resource_json["object"]
)
)