diff --git a/README.md b/README.md index a6b3dfa..5d04b24 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,19 @@ `qbreader` is a Python wrapper to the qbreader API as well as a general quizbowl library. It provides both asynchronous and synchronous interfaces to the API along with functionality for representing questions. +## Tossup Example + ```py >>> from qbreader import Sync as qbr # synchronous interface ->>> tossup = qbr.random_tossup()[0] +>>> sync_client = qbr() +>>> tossup = sync_client.random_tossup()[0] >>> tossup.question +'Tim Peters wrote 19 “guiding principles” of this programming language, which include the maxim “Complex is better than complicated.” The “pandas” library was written for this language. Unicode string values had to be defined with a “u” in version 2 of this language. Libraries in this language include Tkinter, Tensorflow, (*) NumPy (“numb pie”) and SciPy (“sigh pie”). The framework Django was written in this language. This language uses “duck typing.” Variables in this language are often named “spam” and “eggs.” Guido van Rossum invented, for 10 points, what programming language named for a British comedy troupe?' +>>> tossup.question_sanitized 'Tim Peters wrote 19 “guiding principles” of this programming language, which include the maxim “Complex is better than complicated.” The “pandas” library was written for this language. Unicode string values had to be defined with a “u” in version 2 of this language. Libraries in this language include Tkinter, Tensorflow, (*) NumPy (“numb pie”) and SciPy (“sigh pie”). The framework Django was written in this language. This language uses “duck typing.” Variables in this language are often named “spam” and “eggs.” Guido van Rossum invented, for 10 points, what programming language named for a British comedy troupe?' >>> tossup.answer +'Python' +>>> tossup.answer_sanitized 'Python' >>> tossup.category @@ -26,8 +33,30 @@ both asynchronous and synchronous interfaces to the API along with functionality >>> tossup.difficulty ->>> tossup.set +>>> tossup.set.name '2022 Prison Bowl' ->>> (tossup.packet_number, tossup.question_number) +>>> (tossup.packet.number, tossup.number) (4, 20) ``` + +## Bonus Example + +```py +>>> bonus = sync_client.random_bonus()[0] +>>> bonus.leadin +'The Curry–Howard isomorphism states that computer programs are directly equivalent to these mathematical constructs, which can be automated using the languages Lean or Rocq (“rock”). For 10 points each:' +>>> bonus.leadin_sanitized +'The Curry-Howard isomorphism states that computer programs are directly equivalent to these mathematical constructs, which can be automated using the languages Lean or Rocq ("rock"). For 10 points each:' +>>> bonus.parts +('Name these mathematical constructs that are used to formally demonstrate the truth of a mathematical statement.', 'According to the Curry–Howard isomorphism, these programming concepts correspond to individual propositions of a proof. One method of “inferring” these things in programming languages like Python is named for the duck test.', 'Haskell Curry also lends his name to “currying,” a common tool in functional programming languages that transforms a function into a sequence of functions each with a smaller value for this property. A description is acceptable.') +>>> bonus.parts_sanitized +('Name these mathematical constructs that are used to formally demonstrate the truth of a mathematical statement.', 'According to the Curry-Howard isomorphism, these programming concepts correspond to individual propositions of a proof. One method of "inferring" these things in programming languages like Python is named for the duck test.', 'Haskell Curry also lends his name to "currying," a common tool in functional programming languages that transforms a function into a sequence of functions each with a smaller value for this property. A description is acceptable.') +>>> bonus.answers +('mathematical proofs [or formal proofs or proofs of correctness; accept proof assistant or theorem prover or Rocq prover]', 'data types [accept type inference or duck typing]', 'arity [accept descriptions of the number of arguments or the number of parameters or the number of inputs of a function]') +>>> bonus.answers_sanitized +('mathematical proofs [or formal proofs or proofs of correctness; accept proof assistant or theorem prover or Rocq prover]', 'data types [accept type inference or duck typing]', 'arity [accept descriptions of the number of arguments or the number of parameters or the number of inputs of a function]') +>>> bonus.difficultyModifiers +('e', 'm', 'h') +>>> bonus.values +(10, 10, 10) +``` diff --git a/poetry.lock b/poetry.lock index 9e8f2bc..74e5d67 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "aiohttp" @@ -1458,5 +1458,5 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" -python-versions = "^3.11" -content-hash = "789e828d581093fa2d0a6bb185d3ee61e3776e362d089bb691b1a04b09bbe279" +python-versions = "~3.11" +content-hash = "469cc03b8ed58ce11896446c961cdbaf3ba0967bf3a784acb0a675ec5857228e" diff --git a/pyproject.toml b/pyproject.toml index ac70a42..6fd0996 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "qbreader" -version = "1.0.0-rc.2" +version = "1.0.0-rc.3" description = "Quizbowl library and Python wrapper for the qbreader API" authors = [ "Sky \"g3ner1c\" Hong ", @@ -27,7 +27,7 @@ classifiers = [ ] [tool.poetry.dependencies] -python = "^3.11" +python = "~3.11" requests = "^2.31.0" aiohttp = "^3.8.4" diff --git a/qbreader/asynchronous.py b/qbreader/asynchronous.py index 812d62d..55cc767 100644 --- a/qbreader/asynchronous.py +++ b/qbreader/asynchronous.py @@ -19,6 +19,7 @@ UnnormalizedCategory, UnnormalizedDifficulty, UnnormalizedSubcategory, + Year, ) @@ -210,8 +211,8 @@ async def random_tossup( categories: UnnormalizedCategory = None, subcategories: UnnormalizedSubcategory = None, number: int = 1, - min_year: int = 2010, - max_year: int = 2023, + min_year: int = Year.MIN_YEAR, + max_year: int = Year.CURRENT_YEAR, ) -> tuple[Tossup, ...]: """Get random tossups from the database. @@ -231,9 +232,9 @@ async def random_tossup( between categories and subcategories. number : int, default = 1 The number of tossups to return. - min_year : int, default = 2010 + min_year : int, default = Year.MIN_YEAR The oldest year to search for. - max_year : int, default = 2023 + max_year : int, default = Year.CURRENT_YEAR The most recent year to search for. Returns @@ -280,8 +281,8 @@ async def random_bonus( categories: UnnormalizedCategory = None, subcategories: UnnormalizedSubcategory = None, number: int = 1, - min_year: int = 2010, - max_year: int = 2023, + min_year: int = Year.MIN_YEAR, + max_year: int = Year.CURRENT_YEAR, three_part_bonuses: bool = False, ) -> tuple[Bonus, ...]: """Get random bonuses from the database. @@ -302,9 +303,9 @@ async def random_bonus( between categories and subcategories. number : int, default = 1 The number of bonuses to return. - min_year : int, default = 2010 + min_year : int, default = Year.MIN_YEAR The oldest year to search for. - max_year : int, default = 2023 + max_year : int, default = Year.CURRENT_YEAR The most recent year to search for. three_part_bonuses : bool, default = False Whether to only return bonuses with 3 parts. diff --git a/qbreader/synchronous.py b/qbreader/synchronous.py index 54c985b..ae5b426 100644 --- a/qbreader/synchronous.py +++ b/qbreader/synchronous.py @@ -19,6 +19,7 @@ UnnormalizedCategory, UnnormalizedDifficulty, UnnormalizedSubcategory, + Year, ) @@ -175,8 +176,8 @@ def random_tossup( categories: UnnormalizedCategory = None, subcategories: UnnormalizedSubcategory = None, number: int = 1, - min_year: int = 2010, - max_year: int = 2023, + min_year: int = Year.MIN_YEAR, + max_year: int = Year.CURRENT_YEAR, ) -> tuple[Tossup, ...]: """Get random tossups from the database. @@ -196,9 +197,9 @@ def random_tossup( between categories and subcategories. number : int, default = 1 The number of tossups to return. - min_year : int, default = 2010 + min_year : int, default = Year.MIN_YEAR The oldest year to search for. - max_year : int, default = 2023 + max_year : int, default = Year.CURRENT_YEAR The most recent year to search for. Returns @@ -245,8 +246,8 @@ def random_bonus( categories: UnnormalizedCategory = None, subcategories: UnnormalizedSubcategory = None, number: int = 1, - min_year: int = 2010, - max_year: int = 2023, + min_year: int = Year.MIN_YEAR, + max_year: int = Year.CURRENT_YEAR, three_part_bonuses: bool = False, ) -> tuple[Bonus, ...]: """Get random bonuses from the database. @@ -267,9 +268,9 @@ def random_bonus( between categories and subcategories. number : int, default = 1 The number of bonuses to return. - min_year : int, default = 2010 + min_year : int, default = Year.MIN_YEAR The oldest year to search for. - max_year : int, default = 2023 + max_year : int, default = Year.CURRENT_YEAR The most recent year to search for. three_part_bonuses : bool, default = False Whether to only return bonuses with 3 parts. diff --git a/qbreader/types.py b/qbreader/types.py index 27c4a22..a73d185 100644 --- a/qbreader/types.py +++ b/qbreader/types.py @@ -84,6 +84,14 @@ class Difficulty(enum.StrEnum): OPEN = "10" +class DifficultyModifier(enum.StrEnum): + """Difficulty modifier enum.""" + + EASY = "e" + MEDIUM = "m" + HARD = "h" + + class Directive(enum.StrEnum): """Directives given by `api/check-answer`.""" @@ -92,6 +100,13 @@ class Directive(enum.StrEnum): PROMPT = "prompt" +class Year(enum.IntEnum): + """Min/max year enum""" + + MIN_YEAR = 2010 + CURRENT_YEAR = 2024 + + class AnswerJudgement: """A judgement given by `api/check-answer`.""" @@ -219,26 +234,26 @@ class Tossup: def __init__( self: Self, question: str, - formatted_answer: Optional[str], + question_sanitized: str, answer: str, + answer_sanitized: str, + difficulty: Difficulty, category: Category, subcategory: Subcategory, - set: str, - year: int, - packet_number: int, - question_number: int, - difficulty: Difficulty, + packet: PacketMetadata, + set: SetMetadata, + number: int, ): self.question: str = question - self.formatted_answer: str = formatted_answer if formatted_answer else answer + self.question_sanitized: str = question_sanitized self.answer: str = answer + self.answer_sanitized: str = answer_sanitized + self.difficulty: Difficulty = difficulty self.category: Category = category self.subcategory: Subcategory = subcategory - self.set: str = set - self.year: int = year - self.packet_number: int = packet_number - self.question_number: int = question_number - self.difficulty: Difficulty = difficulty + self.packet: PacketMetadata = packet + self.set: SetMetadata = set + self.number: int = number @classmethod def from_json(cls: Type[Self], json: dict[str, Any]) -> Self: @@ -248,27 +263,27 @@ def from_json(cls: Type[Self], json: dict[str, Any]) -> Self: """ return cls( question=json["question"], - formatted_answer=json.get("formatted_answer", json["answer"]), + question_sanitized=json["question_sanitized"], answer=json["answer"], + answer_sanitized=json["answer_sanitized"], + difficulty=Difficulty(str(json["difficulty"])), category=Category(json["category"]), subcategory=Subcategory(json["subcategory"]), - set=json["setName"], - year=json["setYear"], - packet_number=json["packetNumber"], - question_number=json["questionNumber"], - difficulty=Difficulty(str(json["difficulty"])), + packet=PacketMetadata.from_json(json["packet"]), + set=SetMetadata.from_json(json["set"]), + number=json["number"], ) def check_answer_sync(self, givenAnswer: str) -> AnswerJudgement: """Check whether an answer is correct.""" - return AnswerJudgement.check_answer_sync(self.formatted_answer, givenAnswer) + return AnswerJudgement.check_answer_sync(self.answer, givenAnswer) async def check_answer_async( self, givenAnswer: str, session: aiohttp.ClientSession | None = None ) -> AnswerJudgement: """Asynchronously check whether an answer is correct.""" return await AnswerJudgement.check_answer_async( - self.formatted_answer, givenAnswer, session + self.answer, givenAnswer, session ) def __eq__(self, other: object) -> bool: @@ -278,15 +293,15 @@ def __eq__(self, other: object) -> bool: return ( self.question == other.question - and self.formatted_answer == other.formatted_answer + and self.question_sanitized == other.question_sanitized and self.answer == other.answer + and self.answer_sanitized == other.answer_sanitized + and self.difficulty == other.difficulty and self.category == other.category and self.subcategory == other.subcategory + and self.packet == other.packet and self.set == other.set - and self.year == other.year - and self.packet_number == other.packet_number - and self.question_number == other.question_number - and self.difficulty == other.difficulty + and self.number == other.number ) def __str__(self) -> str: @@ -300,30 +315,36 @@ class Bonus: def __init__( self: Self, leadin: str, + leadin_sanitized: str, parts: Sequence[str], - formatted_answers: Optional[Sequence[str]], + parts_sanitized: Sequence[str], answers: Sequence[str], + answers_sanitized: Sequence[str], + difficulty: Difficulty, category: Category, subcategory: Subcategory, - set: str, - year: int, - packet_number: int, - question_number: int, - difficulty: Difficulty, + set: SetMetadata, + packet: PacketMetadata, + number: int, + values: Optional[Sequence[int]] = None, + difficultyModifiers: Optional[Sequence[DifficultyModifier]] = None, ): self.leadin: str = leadin + self.leadin_sanitized: str = leadin_sanitized self.parts: tuple[str, ...] = tuple(parts) - self.formatted_answers: tuple[str, ...] = tuple( - formatted_answers if formatted_answers else answers - ) + self.parts_sanitized: tuple[str, ...] = tuple(parts_sanitized) self.answers: tuple[str, ...] = tuple(answers) + self.answers_sanitized: tuple[str, ...] = tuple(answers_sanitized) + self.difficulty: Difficulty = difficulty self.category: Category = category self.subcategory: Subcategory = subcategory - self.set: str = set - self.year: int = year - self.packet_number: int = packet_number - self.question_number: int = question_number - self.difficulty: Difficulty = difficulty + self.set: SetMetadata = set + self.packet: PacketMetadata = packet + self.number: int = number + self.values: Optional[tuple[int, ...]] = tuple(values) if values else None + self.difficultyModifiers: Optional[tuple[DifficultyModifier, ...]] = ( + tuple(difficultyModifiers) if difficultyModifiers else None + ) @classmethod def from_json(cls: Type[Self], json: dict[str, Any]) -> Self: @@ -333,30 +354,31 @@ def from_json(cls: Type[Self], json: dict[str, Any]) -> Self: """ return cls( leadin=json["leadin"], + leadin_sanitized=json["leadin_sanitized"], parts=json["parts"], - formatted_answers=json.get("formatted_answers", json["answers"]), + parts_sanitized=json["parts_sanitized"], answers=json["answers"], + answers_sanitized=json["answers_sanitized"], + difficulty=Difficulty(str(json["difficulty"])), category=Category(json["category"]), subcategory=Subcategory(json["subcategory"]), - set=json["setName"], - year=json["setYear"], - packet_number=json["packetNumber"], - question_number=json["questionNumber"], - difficulty=Difficulty(str(json["difficulty"])), + set=SetMetadata.from_json(json["set"]), + packet=PacketMetadata.from_json(json["packet"]), + number=json["number"], + values=json.get("values", None), + difficultyModifiers=json.get("difficultyModifiers", None), ) def check_answer_sync(self, part: int, givenAnswer: str) -> AnswerJudgement: """Check whether an answer is correct.""" - return AnswerJudgement.check_answer_sync( - self.formatted_answers[part], givenAnswer - ) + return AnswerJudgement.check_answer_sync(self.answers[part], givenAnswer) async def check_answer_async( self, part: int, givenAnswer: str, session: aiohttp.ClientSession ) -> AnswerJudgement: """Asynchronously check whether an answer is correct.""" return await AnswerJudgement.check_answer_async( - self.formatted_answers[part], givenAnswer, session + self.answers[part], givenAnswer, session ) def __eq__(self, other: object) -> bool: @@ -366,16 +388,19 @@ def __eq__(self, other: object) -> bool: return ( self.leadin == other.leadin + and self.leadin_sanitized == other.leadin_sanitized and self.parts == other.parts - and self.formatted_answers == other.formatted_answers + and self.parts_sanitized == other.parts_sanitized and self.answers == other.answers + and self.answers_sanitized == other.answers_sanitized + and self.difficulty == other.difficulty and self.category == other.category and self.subcategory == other.subcategory and self.set == other.set - and self.year == other.year - and self.packet_number == other.packet_number - and self.question_number == other.question_number - and self.difficulty == other.difficulty + and self.packet == other.packet + and self.number == other.number + and self.values == other.values + and self.difficultyModifiers == other.difficultyModifiers ) def __str__(self) -> str: @@ -440,9 +465,9 @@ def __init__( ): self.tossups: tuple[Tossup, ...] = tuple(tossups) self.bonuses: tuple[Bonus, ...] = tuple(bonuses) - self.number: Optional[int] = number - self.name: Optional[str] = name if name else self.tossups[0].set - self.year: Optional[int] = year if year else self.tossups[0].year + self.number: Optional[int] = number if number else self.tossups[0].packet.number + self.name: Optional[str] = name if name else self.tossups[0].set.name + self.year: Optional[int] = year if year else self.tossups[0].set.year @classmethod def from_json( @@ -488,6 +513,76 @@ def __str__(self) -> str: ) +class PacketMetadata: + def __init__( + self: Self, + _id: str, + name: str, + number: int, + ): + self._id: str = _id + self.name: str = name + self.number: int = number + + @classmethod + def from_json(cls: Type[Self], json: dict[str, Any]) -> Self: + return cls( + _id=json["_id"], + name=json["name"], + number=json["number"], + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, PacketMetadata): + return NotImplemented + + return ( + self._id == other._id + and self.name == other.name + and self.number == other.number + ) + + def __str__(self) -> str: + return self.name + + +class SetMetadata: + def __init__( + self: Self, + _id: str, + name: str, + year: int, + standard: bool, + ): + self._id: str = _id + self.name: str = name + self.year: int = year + self.standard: bool = standard + + @classmethod + def from_json(cls: Type[Self], json: dict[str, Any]) -> Self: + return cls( + _id=json["_id"], + name=json["name"], + year=json["year"], + standard=json["standard"], + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SetMetadata): + return NotImplemented + + return ( + self._id == other._id + and self.name == other.name + and self.year == other.year + and self.standard == other.standard + ) + + def __str__(self) -> str: + return self.name + + QuestionType: TypeAlias = Union[ Literal["tossup", "bonus", "all"], Type[Tossup], Type[Bonus] ] diff --git a/tests/__init__.py b/tests/__init__.py index a06ba42..d22fa50 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -10,6 +10,8 @@ def check_internet_connection(): urlopen("https://www.qbreader.org") return True except Exception: + # you may want to check if you've installed SSL certificates + # https://stackoverflow.com/questions/44649449/brew-installation-of-python-3-6-1-ssl-certificate-verify-failed-certificate return diff --git a/tests/test_types.py b/tests/test_types.py index 703af05..a2b8fac 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,31 +1,31 @@ """Test the types, classes, and structures used by the qbreader library.""" - import qbreader as qb -from qbreader.types import Bonus, Tossup +from qbreader.types import Bonus, Tossup, PacketMetadata, SetMetadata class TestTossup: """Test the Tossup class.""" tu_json = { - "_id": "64a0ccb31634f2d5eb7df02a", - "question": "This general class of devices could move between hypothetical castles via up and down escalators in a cycler. These devices can transfer their angular momentum to two “yo-yo” masses attached with cords in order to decrease their rotation rate. The Oberth effect describes how these devices gain kinetic energy more efficiently while at (*) periapsis. Hohmann transfers and bi-elliptic transfers are used to adjust the altitude of these devices. The trajectory-dependent delta-v budget of one of these devices can be reduced by using a slingshot maneuver. Station-keeping can help these objects remain geosynchronous. For 10 points, name this general class of devices used to collect astronomical data, such as Voyager 1.", # noqa: E501 - "formatted_answer": "spacecraft [or spaceships; accept space probes, satellites, or space stations, or space vehicles; prompt with rockets or thrusters with, “To what devices are rockets attached?”] (The first clue describes the Aldrin cycle, proposed by Buzz Aldrin.)", # noqa: E501 - "answer": "spacecraft [or spaceships; accept space probes, satellites, or space stations, or space vehicles; prompt with rockets or thrusters with, “To what devices are rockets attached?”] (The first clue describes the Aldrin cycle, proposed by Buzz Aldrin.)", # noqa: E501 + "_id": "64046cc6de59b8af97422da5", + "question": "Radiative power is inversely proportional to this quantity cubed, times 6-pi-epsilon, according to the Larmor formula. This quantity is in the numerator in the formula for the index of refraction. When a charged particle exceeds this quantity while in a medium, it produces Cherenkov radiation. This (*) quantity is equal to one divided by the square root of the product of the vacuum permittivity and permeability. This quantity is constant in all inertial reference frames. For 10 points, name this value symbolized c, that is about 30 million meters per second.", # noqa: E501 + "answer": "Speed of Light", "category": "Science", - "subcategory": "Other Science", - "packet": "64a0ccb31634f2d5eb7df01e", - "set": "64a0ccb31634f2d5eb7deed5", - "setName": "2023 MRNA", - "setYear": 2023, - "type": "tossup", - "packetNumber": 9, - "packetName": "09", - "questionNumber": 12, - "createdAt": "2023-07-02T01:02:43.628Z", - "updatedAt": "2023-07-02T01:03:31.712Z", - "difficulty": 7, + "subcategory": "Physics", + "packet": {"_id": "64046cc6de59b8af97422da2", "name": "03", "number": 3}, + "set": { + "_id": "64046cc6de59b8af97422d4f", + "name": "2017 WHAQ", + "year": 2017, + "standard": True, + }, + "createdAt": "2023-03-05T10:19:50.469Z", + "updatedAt": "2024-11-24T22:47:40.013Z", + "difficulty": 3, + "number": 3, + "answer_sanitized": "Speed of Light", + "question_sanitized": "Radiative power is inversely proportional to this quantity cubed, times 6-pi-epsilon, according to the Larmor formula. This quantity is in the numerator in the formula for the index of refraction. When a charged particle exceeds this quantity while in a medium, it produces Cherenkov radiation. This (*) quantity is equal to one divided by the square root of the product of the vacuum permittivity and permeability. This quantity is constant in all inertial reference frames. For 10 points, name this value symbolized c, that is about 30 million meters per second.", # noqa: E501 } def test_from_json(self): @@ -49,37 +49,49 @@ class TestBonus: """Test the Bonus class.""" b_json = { - "_id": "644932c99f0045ff841d6792", - "leadin": "Using this metal as a charge carrier avoids both cross-contamination and electrodeposition because it has four stable oxidation states: plus-two, plus-three, plus-four, and plus-five. For 10 points each:", # noqa: E501 + "_id": "673ec00f90236da031c2cedb", + "leadin": "With George Jean Nathan, H. L. Mencken co-founded a newspaper called The [this adjective] Mercury, which eventually fell under far-right leadership. For 10 points each:", # noqa: E501 + "leadin_sanitized": "With George Jean Nathan, H. L. Mencken co-founded a newspaper called The [this adjective] Mercury, which eventually fell under far-right leadership. For 10 points each:", # noqa: E501 "parts": [ - "Name this transition metal used as the charge carrier in the most common redox flow battery for electric grids.", # noqa: E501 - "To balance charge between the pipes, redox flow batteries rely on one of these semipermeable barriers made of Nafion. They also separate the compartments of fuel cells.", # noqa: E501 - "The electrolyte of a vanadium redox flow battery is a solution of this compound. This “acid” in a lead-acid battery is prepared using a vanadium catalyst in the contact process.", # noqa: E501 + "Name this adjective in the title of a Mencken book that pays homage to Noah Webster. That book claims that the sentence “who are you talking to” is “doubly” this adjective since it forgoes “whom” and puts a preposition at the end of a sentence.", # noqa: E501 + " The Baltimore Sun sent Mencken to cover one of these events in Dayton, Tennessee, where he gave it a famous nickname. That event of this type was fictionalized in the play Inherit the Wind.", # noqa: E501 + "At the end of Inherit the Wind, Henry Drummond picks up a book by Darwin in one hand and this book with the other. Mencken claimed to have coined the term for a “Belt” in the Southern United States named for this text.", # noqa: E501 + ], + "parts_sanitized": [ + 'Name this adjective in the title of a Mencken book that pays homage to Noah Webster. That book claims that the sentence "who are you talking to" is "doubly" this adjective since it forgoes "whom" and puts a preposition at the end of a sentence.', # noqa: E501 + "The Baltimore Sun sent Mencken to cover one of these events in Dayton, Tennessee, where he gave it a famous nickname. That event of this type was fictionalized in the play Inherit the Wind.", # noqa: E501 + 'At the end of Inherit the Wind, Henry Drummond picks up a book by Darwin in one hand and this book with the other. Mencken claimed to have coined the term for a "Belt" in the Southern United States named for this text.', # noqa: E501 ], - "values": [], "answers": [ - "vanadium [or V]", - "proton-exchange membranes [or PEMs; or polymer-electrolyte membranes; prompt on membranes orion-exchange membranes]", # noqa: E501 - "sulfuric acid [or H2SO4]", + "American [accept The American Mercury or The American Language]", # noqa: E501 + "trial [accept Scopes trial or Scopes Monkey trial]", # noqa: E501 + "the Bible ", ], - "formatted_answers": [ - "vanadium [or V]", - "proton-exchange membranes [or PEMs; or polymer-electrolyte membranes; prompt on membranes orion-exchange membranes]", # noqa: E501 - "sulfuric acid [or H2SO4]", + "answers_sanitized": [ + "American [accept The American Mercury or The American Language]", + "trial [accept Scopes trial or Scopes Monkey trial]", + "the Bible", ], - "category": "Science", - "subcategory": "Chemistry", - "packet": "644932c99f0045ff841d6777", - "set": "644932c99f0045ff841d66fb", - "setName": "2023 ACF Nationals", - "setYear": 2023, - "type": "bonus", - "packetNumber": 4, - "packetName": "Finals 2. Editors 12", - "questionNumber": 7, - "createdAt": "2023-04-26T14:18:49.683Z", - "updatedAt": "2023-04-26T14:19:23.999Z", - "difficulty": 9, + "updatedAt": "2024-11-21T05:07:27.318Z", + "category": "Literature", + "subcategory": "American Literature", + "alternate_subcategory": "Misc Literature", + "values": [10, 10, 10], + "difficultyModifiers": ["h", "m", "e"], + "number": 1, + "createdAt": "2024-11-21T05:07:27.318Z", + "difficulty": 7, + "packet": { + "_id": "673ec00f90236da031c2cec6", + "name": "A - Claremont A, Edinburgh A, Haverford A, Georgia Tech B, Illinois C, Michigan B", # noqa: E501 + "number": 1, + }, + "set": { + "_id": "673ec00f90236da031c2cec5", + "name": "2024 ACF Winter", + "year": 2024, + "standard": True, + }, } def test_from_json(self): @@ -121,6 +133,38 @@ def test_iter(self): assert b == self.packet.bonuses[i] +class TestPacketMetadata: + """Test the PacketMetadata class.""" + + packetMetadata = PacketMetadata.from_json(TestTossup.tu_json["packet"]) + + def test_eq(self): + """Test the __eq__ method.""" + p1 = p2 = self.packetMetadata + assert p1 == p2 + assert p1 != "not packet metadata" + + def test_str(self): + """Test the __str__ method.""" + assert str(self.packetMetadata) + + +class TestSetMetadata: + """Test the SetMetadata class.""" + + setMetadata = SetMetadata.from_json(TestTossup.tu_json["set"]) + + def test_eq(self): + """Test the __eq__ method.""" + s1 = s2 = self.setMetadata + assert s1 == s2 + assert s1 != "not set metadata" + + def test_str(self): + """Test the __str__ method.""" + assert str(self.setMetadata) + + class TestQueryResponse: """Test the QueryResponse class."""