From f74483d6b704663557505e265247dae65ecd3927 Mon Sep 17 00:00:00 2001 From: sreysus Date: Fri, 17 Jan 2025 10:22:41 -0500 Subject: [PATCH 1/9] feat-wip(alt-subcat-support)!: add functionality for random-tossup - modified `_api_utils.py` - breaking change to `normalize_subcats()` function --- qbreader/_api_utils.py | 71 ++++++++++++++++++++++++++++++++++++++--- qbreader/synchronous.py | 19 ++++++++--- qbreader/types.py | 48 ++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 8 deletions(-) diff --git a/qbreader/_api_utils.py b/qbreader/_api_utils.py index daf6074..7926117 100644 --- a/qbreader/_api_utils.py +++ b/qbreader/_api_utils.py @@ -4,15 +4,17 @@ import warnings from enum import Enum, EnumType -from typing import Iterable, Optional, Union +from typing import Iterable, Optional, Union, Tuple from qbreader.types import ( - Category, Difficulty, + Category, Subcategory, + AlternateSubcategory, UnnormalizedCategory, UnnormalizedDifficulty, UnnormalizedSubcategory, + UnnormalizedAlternateSubcategory, ) @@ -97,9 +99,70 @@ def normalize_cat(unnormalized_cats: UnnormalizedCategory): return normalize_enumlike(unnormalized_cats, Category) -def normalize_subcat(unnormalized_subcats: UnnormalizedSubcategory): +def subcategory_correspondence(typed_alt_subcat: AlternateSubcategory) -> Subcategory: + + if typed_alt_subcat in [ + AlternateSubcategory.ASTRONOMY, + AlternateSubcategory.COMPUTER_SCIENCE, + AlternateSubcategory.MATH, + AlternateSubcategory.EARTH_SCIENCE, + AlternateSubcategory.ENGINEERING, + AlternateSubcategory.MISC_SCIENCE + ]: + return Subcategory.OTHER_SCIENCE + + if typed_alt_subcat in [ + AlternateSubcategory.ARCHITECTURE, + AlternateSubcategory.DANCE, + AlternateSubcategory.FILM, + AlternateSubcategory.JAZZ, + AlternateSubcategory.OPERA, + AlternateSubcategory.PHOTOGRAPHY, + AlternateSubcategory.MISC_ARTS + ]: + return Subcategory.OTHER_FINE_ARTS + + if typed_alt_subcat in [ + AlternateSubcategory.DRAMA, + AlternateSubcategory.LONG_FICTION, + AlternateSubcategory.POETRY, + AlternateSubcategory.SHORT_FICTION, + AlternateSubcategory.MISC_LITERATURE + ]: + return Subcategory.OTHER_LITERATURE + + +def normalize_subcats( + unnormalized_subcats: UnnormalizedSubcategory, + unnormalized_alt_subcats: UnnormalizedAlternateSubcategory +) -> Tuple[Subcategory, AlternateSubcategory]: """Normalize a single or list of subcategories to a comma separated string.""" - return normalize_enumlike(unnormalized_subcats, Subcategory) + + typed_alt_subcats: list[AlternateSubcategory] = [] + + if isinstance(unnormalized_alt_subcats, str): + typed_alt_subcats.append(AlternateSubcategory(unnormalized_alt_subcats)) + elif isinstance(unnormalized_alt_subcats, Iterable): + for alt_subcat in unnormalized_alt_subcats: + typed_alt_subcats.append(AlternateSubcategory(alt_subcat)) + + + to_be_pushed_subcats: list[Subcategory] = [] + + for alt_subcat in typed_alt_subcats: + to_be_pushed_subcats.append(subcategory_correspondence(alt_subcat)) + + final_subcats = [] + if unnormalized_subcats is None: + final_subcats = to_be_pushed_subcats + elif isinstance(unnormalized_subcats, str): + final_subcats = [Subcategory(unnormalized_alt_subcats), *to_be_pushed_subcats] + elif isinstance(unnormalized_subcats, Iterable): + for alt_subcat in unnormalized_subcats: + final_subcats.append(Subcategory(alt_subcat)) + final_subcats.append(*to_be_pushed_subcats) + + return (normalize_enumlike(final_subcats, Subcategory), normalize_enumlike(typed_alt_subcats, AlternateSubcategory)) def prune_none(params: dict) -> dict: diff --git a/qbreader/synchronous.py b/qbreader/synchronous.py index ae5b426..38b6475 100644 --- a/qbreader/synchronous.py +++ b/qbreader/synchronous.py @@ -16,9 +16,10 @@ QuestionType, SearchType, Tossup, - UnnormalizedCategory, UnnormalizedDifficulty, + UnnormalizedCategory, UnnormalizedSubcategory, + UnnormalizedAlternateSubcategory, Year, ) @@ -40,6 +41,7 @@ def query( difficulties: UnnormalizedDifficulty = None, categories: UnnormalizedCategory = None, subcategories: UnnormalizedSubcategory = None, + alternate_subcategories: UnnormalizedAlternateSubcategory = None, maxReturnLength: Optional[int] = 25, tossupPagination: Optional[int] = 1, bonusPagination: Optional[int] = 1, @@ -79,6 +81,10 @@ class type. The subcategories to search for. Can be a single or an array of `Subcategory` enums or strings. The API does not check for consistency between categories and subcategories. + alternate_subcategories: qbreaader.types.UnnormalizedAlternateSubcategory, optional + The alternates subcategories to search for. Can be a single or an array of + `AlternateSubcategory` enum variants or strings. The API does not check for consistency + between categories, subcategories, and alternate subcategories. maxReturnLength : int, default = 25 The maximum number of questions to return. tossupPagination : int, default = 1 @@ -175,6 +181,7 @@ def random_tossup( difficulties: UnnormalizedDifficulty = None, categories: UnnormalizedCategory = None, subcategories: UnnormalizedSubcategory = None, + alternate_subcategories: UnnormalizedAlternateSubcategory = None, number: int = 1, min_year: int = Year.MIN_YEAR, max_year: int = Year.CURRENT_YEAR, @@ -223,14 +230,18 @@ def random_tossup( url = BASE_URL + "/random-tossup" + (normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_subcats(subcategories, alternate_subcategories) + data = { "difficulties": api_utils.normalize_diff(difficulties), "categories": api_utils.normalize_cat(categories), - "subcategories": api_utils.normalize_subcat(subcategories), + "subcategories": normalized_subcategories, + "alternateSubcategories": normalized_alternate_subcategories, "number": number, - "min_year": min_year, - "max_year": max_year, + "minYear": min_year, + "maxYear": max_year, } + data = api_utils.prune_none(data) response: requests.Response = requests.get(url, params=data) diff --git a/qbreader/types.py b/qbreader/types.py index 903afc3..845c115 100644 --- a/qbreader/types.py +++ b/qbreader/types.py @@ -67,6 +67,38 @@ class Subcategory(enum.StrEnum): AUDITORY_FINE_ARTS = "Auditory Fine Arts" OTHER_FINE_ARTS = "Other Fine Arts" +class AlternateSubcategory(enum.StrEnum): + """Question alternate subcategory enum.""" + + DRAMA = "Drama" + LONG_FICTION = "Long Fiction" + POETRY = "Poetry" + SHORT_FICTION = "Short Fiction" + MISC_LITERATURE = "Misc Literature" + + MATH = "Math" + ASTRONOMY = "Astronomy" + COMPUTER_SCIENCE = "Computer Science" + EARTH_SCIENCE = "Earth Science" + ENGINEERING = "Engineering" + MISC_SCIENCE = "Misc Science" + + ARCHITECTURE = "Architecture" + DANCE = "Dance" + FILM = "Film" + JAZZ = "Jazz" + OPERA = "Opera" + PHOTOGRAPHY = "Photography" + MISC_ARTS = "Misc Arts" + + ANTHROPOLOGY = "Anthropology" + ECONOMICS = "Economics" + LINGUISTICS = "Linguistics" + PSYCHOLOGY = "Psychology" + SOCIOLOGY = "Sociology" + OTHER_SOCIAL_SCIENCE = "Other Social Science" + + NONE = "None" class Difficulty(enum.StrEnum): """Question difficulty enum.""" @@ -240,6 +272,7 @@ def __init__( difficulty: Difficulty, category: Category, subcategory: Subcategory, + alternate_subcategory: AlternateSubcategory, packet: PacketMetadata, set: SetMetadata, number: int, @@ -251,6 +284,7 @@ def __init__( self.difficulty: Difficulty = difficulty self.category: Category = category self.subcategory: Subcategory = subcategory + self.alternate_subcategory: AlternateSubcategory = alternate_subcategory self.packet: PacketMetadata = packet self.set: SetMetadata = set self.number: int = number @@ -269,6 +303,7 @@ def from_json(cls: Type[Self], json: dict[str, Any]) -> Self: difficulty=Difficulty(str(json["difficulty"])), category=Category(json["category"]), subcategory=Subcategory(json["subcategory"]), + alternate_subcategory=AlternateSubcategory(json.get("alternate_subcategory", "None")), packet=PacketMetadata.from_json(json["packet"]), set=SetMetadata.from_json(json["set"]), number=json["number"], @@ -299,6 +334,7 @@ def __eq__(self, other: object) -> bool: and self.difficulty == other.difficulty and self.category == other.category and self.subcategory == other.subcategory + and self.alternate_subcategory == other.alternate_subcategory and self.packet == other.packet and self.set == other.set and self.number == other.number @@ -323,6 +359,7 @@ def __init__( difficulty: Difficulty, category: Category, subcategory: Subcategory, + alternate_subcategory: AlternateSubcategory, set: SetMetadata, packet: PacketMetadata, number: int, @@ -338,6 +375,7 @@ def __init__( self.difficulty: Difficulty = difficulty self.category: Category = category self.subcategory: Subcategory = subcategory + self.alternate_subcategory: AlternateSubcategory = alternate_subcategory self.set: SetMetadata = set self.packet: PacketMetadata = packet self.number: int = number @@ -362,6 +400,7 @@ def from_json(cls: Type[Self], json: dict[str, Any]) -> Self: difficulty=Difficulty(str(json["difficulty"])), category=Category(json["category"]), subcategory=Subcategory(json["subcategory"]), + alternate_subcategory=AlternateSubcategory(json["alternate_subcategory"]), set=SetMetadata.from_json(json["set"]), packet=PacketMetadata.from_json(json["packet"]), number=json["number"], @@ -396,6 +435,7 @@ def __eq__(self, other: object) -> bool: and self.difficulty == other.difficulty and self.category == other.category and self.subcategory == other.subcategory + and self.alternate_subcategory == other.alternate_subcategory and self.set == other.set and self.packet == other.packet and self.number == other.number @@ -635,6 +675,12 @@ def __str__(self) -> str: """Type alias for unnormalized subcategories. Union of `Subcategory`, `str`, and `collections.abc.Iterable` containing either.""" +UnnormalizedAlternateSubcategory: TypeAlias = Optional[ + Union[AlternateSubcategory, str, Iterable[Union[AlternateSubcategory, str]]] +] +"""Type alias for unnormalized alternate subcategories. Union of `AlternateSubcategory`, `str`, and +`collections.abc.Iterable` containing either.""" + __all__ = ( "Tossup", @@ -644,6 +690,7 @@ def __str__(self) -> str: "AnswerJudgement", "Category", "Subcategory", + "AlternateSubcategory", "Difficulty", "Directive", "QuestionType", @@ -652,4 +699,5 @@ def __str__(self) -> str: "UnnormalizedDifficulty", "UnnormalizedCategory", "UnnormalizedSubcategory", + "UnnormalizedAlternateSubcategory" ) From bfd2240bff4544a9a3dd92ae7f07040f490bc051 Mon Sep 17 00:00:00 2001 From: sreysus Date: Fri, 17 Jan 2025 10:37:03 -0500 Subject: [PATCH 2/9] feat(alt-subcat-support): works for bonuses, tossups, through both `Sync` and `Async` clients. --- qbreader/asynchronous.py | 35 +++++++++++++++++++++++++++++------ qbreader/synchronous.py | 31 +++++++++++++++++++++++-------- 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/qbreader/asynchronous.py b/qbreader/asynchronous.py index 55cc767..1b2d8eb 100644 --- a/qbreader/asynchronous.py +++ b/qbreader/asynchronous.py @@ -19,6 +19,7 @@ UnnormalizedCategory, UnnormalizedDifficulty, UnnormalizedSubcategory, + UnnormalizedAlternateSubcategory, Year, ) @@ -75,6 +76,7 @@ async def query( difficulties: UnnormalizedDifficulty = None, categories: UnnormalizedCategory = None, subcategories: UnnormalizedSubcategory = None, + alternate_subcategories: UnnormalizedAlternateSubcategory = None, maxReturnLength: Optional[int] = 25, tossupPagination: Optional[int] = 1, bonusPagination: Optional[int] = 1, @@ -114,6 +116,10 @@ class type. The subcategories to search for. Can be a single or an array of `Subcategory` enums or strings. The API does not check for consistency between categories and subcategories. + alternate_subcategories : qbreader.types.UnnormalizedAlternateSubcategory, optional + The alternate subcategories to search for. Can be a single or an array of + `AlternateSubcategory` enums or strings. The API does not check for + consistency between categories and subcategories maxReturnLength : int, default = 25 The maximum number of questions to return. tossupPagination : int, default = 1 @@ -192,6 +198,7 @@ class type. "difficulties": api_utils.normalize_diff(difficulties), "categories": api_utils.normalize_cat(categories), "subcategories": api_utils.normalize_subcat(subcategories), + "alternateSubcategories": api_utils.normalize_alt_subcat(alternate_subcategories), "maxReturnLength": maxReturnLength, "tossupPagination": tossupPagination, "bonusPagination": bonusPagination, @@ -210,6 +217,7 @@ async def random_tossup( difficulties: UnnormalizedDifficulty = None, categories: UnnormalizedCategory = None, subcategories: UnnormalizedSubcategory = None, + alternate_subcategories: UnnormalizedAlternateSubcategory = None, number: int = 1, min_year: int = Year.MIN_YEAR, max_year: int = Year.CURRENT_YEAR, @@ -230,6 +238,10 @@ async def random_tossup( The subcategories to search for. Can be a single or an array of `Subcategory` enums or strings. The API does not check for consistency between categories and subcategories. + alternate_subcategories : qbreader.types.UnnormalizedAlternateSubcategory, optional + The alternate subcategories to search for. Can be a single or an array of + `AlternateSubcategory` enums or strings. The API does not check for + consistency between categories and subcategories number : int, default = 1 The number of tossups to return. min_year : int, default = Year.MIN_YEAR @@ -258,13 +270,16 @@ async def random_tossup( url = BASE_URL + "/random-tossup" + (normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_subcats(subcategories, alternate_subcategories) + data = { "difficulties": api_utils.normalize_diff(difficulties), "categories": api_utils.normalize_cat(categories), - "subcategories": api_utils.normalize_subcat(subcategories), + "subcategories": normalized_subcategories, + "alternateSubcategories": normalized_alternate_subcategories, "number": number, - "min_year": min_year, - "max_year": max_year, + "minYear": min_year, + "maxYear": max_year, } data = api_utils.prune_none(data) @@ -280,6 +295,7 @@ async def random_bonus( difficulties: UnnormalizedDifficulty = None, categories: UnnormalizedCategory = None, subcategories: UnnormalizedSubcategory = None, + alternate_subcategories: UnnormalizedAlternateSubcategory = None, number: int = 1, min_year: int = Year.MIN_YEAR, max_year: int = Year.CURRENT_YEAR, @@ -301,6 +317,10 @@ async def random_bonus( The subcategories to search for. Can be a single or an array of `Subcategory` enums or strings. The API does not check for consistency between categories and subcategories. + alternate_subcategories: qbreaader.types.UnnormalizedAlternateSubcategory, optional + The alternates subcategories to search for. Can be a single or an array of + `AlternateSubcategory` enum variants or strings. The API does not check for consistency + between categories, subcategories, and alternate subcategories. number : int, default = 1 The number of bonuses to return. min_year : int, default = Year.MIN_YEAR @@ -337,13 +357,16 @@ async def random_bonus( url = BASE_URL + "/random-bonus" + (normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_subcats(subcategories, alternate_subcategories) + data = { "difficulties": api_utils.normalize_diff(difficulties), "categories": api_utils.normalize_cat(categories), - "subcategories": api_utils.normalize_subcat(subcategories), + "subcategories": normalized_subcategories, + "alternateSubcategories": normalized_alternate_subcategories, "number": number, - "min_year": min_year, - "max_year": max_year, + "minYear": min_year, + "maxYear": max_year, } data = api_utils.prune_none(data) diff --git a/qbreader/synchronous.py b/qbreader/synchronous.py index 38b6475..4942d47 100644 --- a/qbreader/synchronous.py +++ b/qbreader/synchronous.py @@ -73,13 +73,13 @@ class type. The name of the set to search in. difficulties : qbreader.types.UnnormalizedDifficulty, optional The difficulties to search for. Can be a single or an array of `Difficulty` - enums, strings, or integers. + enum variants, strings, or integers. categories : qbreader.types.UnnormalizedCategory, optional The categories to search for. Can be a single or an array of `Category` - enums or strings. + enum variants or strings. subcategories : qbreader.types.UnnormalizedSubcategory, optional The subcategories to search for. Can be a single or an array of - `Subcategory` enums or strings. The API does not check for consistency + `Subcategory` enum variants or strings. The API does not check for consistency between categories and subcategories. alternate_subcategories: qbreaader.types.UnnormalizedAlternateSubcategory, optional The alternates subcategories to search for. Can be a single or an array of @@ -150,6 +150,8 @@ class type. url = BASE_URL + "/query" + (normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_subcats(subcategories, alternate_subcategories) + data = { "questionType": questionType, "searchType": searchType, @@ -162,7 +164,8 @@ class type. "setName": setName, "difficulties": api_utils.normalize_diff(difficulties), "categories": api_utils.normalize_cat(categories), - "subcategories": api_utils.normalize_subcat(subcategories), + "subcategories": normalized_subcategories, + "alternateSubcategories": normalized_alternate_subcategories, "maxReturnLength": maxReturnLength, "tossupPagination": tossupPagination, "bonusPagination": bonusPagination, @@ -202,6 +205,10 @@ def random_tossup( The subcategories to search for. Can be a single or an array of `Subcategory` enums or strings. The API does not check for consistency between categories and subcategories. + alternate_subcategories: qbreaader.types.UnnormalizedAlternateSubcategory, optional + The alternates subcategories to search for. Can be a single or an array of + `AlternateSubcategory` enum variants or strings. The API does not check for consistency + between categories, subcategories, and alternate subcategories. number : int, default = 1 The number of tossups to return. min_year : int, default = Year.MIN_YEAR @@ -241,10 +248,11 @@ def random_tossup( "minYear": min_year, "maxYear": max_year, } - data = api_utils.prune_none(data) + print(data) response: requests.Response = requests.get(url, params=data) + print(response.url) if response.status_code != 200: raise Exception(str(response.status_code) + " bad request") @@ -256,6 +264,7 @@ def random_bonus( difficulties: UnnormalizedDifficulty = None, categories: UnnormalizedCategory = None, subcategories: UnnormalizedSubcategory = None, + alternate_subcategories: UnnormalizedAlternateSubcategory = None, number: int = 1, min_year: int = Year.MIN_YEAR, max_year: int = Year.CURRENT_YEAR, @@ -277,6 +286,10 @@ def random_bonus( The subcategories to search for. Can be a single or an array of `Subcategory` enums or strings. The API does not check for consistency between categories and subcategories. + alternate_subcategories: qbreaader.types.UnnormalizedAlternateSubcategory, optional + The alternates subcategories to search for. Can be a single or an array of + `AlternateSubcategory` enum variants or strings. The API does not check for consistency + between categories, subcategories, and alternate subcategories. number : int, default = 1 The number of bonuses to return. min_year : int, default = Year.MIN_YEAR @@ -313,13 +326,15 @@ def random_bonus( url = BASE_URL + "/random-bonus" + (normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_subcats(subcategories, alternate_subcategories) data = { "difficulties": api_utils.normalize_diff(difficulties), "categories": api_utils.normalize_cat(categories), - "subcategories": api_utils.normalize_subcat(subcategories), + "subcategories": normalized_subcategories, + "alternateSubcategories": normalized_alternate_subcategories, "number": number, - "min_year": min_year, - "max_year": max_year, + "minYear": min_year, + "maxYear": max_year, } data = api_utils.prune_none(data) From ec5ae7cb34ecb7c1c2782143fcfaec26913a3c2b Mon Sep 17 00:00:00 2001 From: Sreyas Sabbani Date: Fri, 17 Jan 2025 11:01:51 -0500 Subject: [PATCH 3/9] fix(oopsies): forgot to unstage things This should be what I intended to PR. --- qbreader/synchronous.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/qbreader/synchronous.py b/qbreader/synchronous.py index 4942d47..134f6fe 100644 --- a/qbreader/synchronous.py +++ b/qbreader/synchronous.py @@ -73,13 +73,13 @@ class type. The name of the set to search in. difficulties : qbreader.types.UnnormalizedDifficulty, optional The difficulties to search for. Can be a single or an array of `Difficulty` - enum variants, strings, or integers. + enums, strings, or integers. categories : qbreader.types.UnnormalizedCategory, optional The categories to search for. Can be a single or an array of `Category` - enum variants or strings. + enums or strings. subcategories : qbreader.types.UnnormalizedSubcategory, optional The subcategories to search for. Can be a single or an array of - `Subcategory` enum variants or strings. The API does not check for consistency + `Subcategory` enums or strings. The API does not check for consistency between categories and subcategories. alternate_subcategories: qbreaader.types.UnnormalizedAlternateSubcategory, optional The alternates subcategories to search for. Can be a single or an array of @@ -250,9 +250,7 @@ def random_tossup( } data = api_utils.prune_none(data) - print(data) response: requests.Response = requests.get(url, params=data) - print(response.url) if response.status_code != 200: raise Exception(str(response.status_code) + " bad request") From a0c9f8d1d43f826a6b0116e264a1b8e838dda094 Mon Sep 17 00:00:00 2001 From: Sreyas Sabbani Date: Fri, 17 Jan 2025 11:05:18 -0500 Subject: [PATCH 4/9] fix(minor): use regular indexing instead of `.get()` --- qbreader/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qbreader/types.py b/qbreader/types.py index 845c115..47056fe 100644 --- a/qbreader/types.py +++ b/qbreader/types.py @@ -303,7 +303,7 @@ def from_json(cls: Type[Self], json: dict[str, Any]) -> Self: difficulty=Difficulty(str(json["difficulty"])), category=Category(json["category"]), subcategory=Subcategory(json["subcategory"]), - alternate_subcategory=AlternateSubcategory(json.get("alternate_subcategory", "None")), + alternate_subcategory=AlternateSubcategory(json["alternate_subcategory"]), packet=PacketMetadata.from_json(json["packet"]), set=SetMetadata.from_json(json["set"]), number=json["number"], From 5dd4f1a1b772e70dd60d343cd727d31824bc8c79 Mon Sep 17 00:00:00 2001 From: Sreyas Sabbani Date: Fri, 17 Jan 2025 11:06:28 -0500 Subject: [PATCH 5/9] fix(minor): remove None variant in `AlternateSubcategory` --- qbreader/types.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qbreader/types.py b/qbreader/types.py index 47056fe..7f6ee93 100644 --- a/qbreader/types.py +++ b/qbreader/types.py @@ -98,8 +98,6 @@ class AlternateSubcategory(enum.StrEnum): SOCIOLOGY = "Sociology" OTHER_SOCIAL_SCIENCE = "Other Social Science" - NONE = "None" - class Difficulty(enum.StrEnum): """Question difficulty enum.""" From 89a0e1a17087a610e770804d3033cc88c4a739fd Mon Sep 17 00:00:00 2001 From: Geoffrey Wu Date: Fri, 17 Jan 2025 14:54:13 -0600 Subject: [PATCH 6/9] fix logic for alternate subcategory --- qbreader/_api_utils.py | 72 ++++++++++++++++++++++++++++------------ qbreader/asynchronous.py | 16 +++++---- qbreader/synchronous.py | 13 ++++---- qbreader/types.py | 14 ++++---- 4 files changed, 74 insertions(+), 41 deletions(-) diff --git a/qbreader/_api_utils.py b/qbreader/_api_utils.py index 7926117..5b64b06 100644 --- a/qbreader/_api_utils.py +++ b/qbreader/_api_utils.py @@ -93,24 +93,29 @@ def normalize_diff(unnormalized_diffs: UnnormalizedDifficulty): """Normalize a single or list of difficulty values to a comma separated string.""" return normalize_enumlike(unnormalized_diffs, Difficulty) - def normalize_cat(unnormalized_cats: UnnormalizedCategory): """Normalize a single or list of categories to a comma separated string.""" return normalize_enumlike(unnormalized_cats, Category) -def subcategory_correspondence(typed_alt_subcat: AlternateSubcategory) -> Subcategory: +def normalize_subcat(unnormalized_subcats: UnnormalizedCategory): + """Normalize a single or list of subcategories to a comma separated string.""" + return normalize_enumlike(unnormalized_subcats, Category) + +def category_correspondence( + typed_alt_subcat: AlternateSubcategory, +) -> Tuple[Category, Subcategory]: if typed_alt_subcat in [ AlternateSubcategory.ASTRONOMY, AlternateSubcategory.COMPUTER_SCIENCE, AlternateSubcategory.MATH, AlternateSubcategory.EARTH_SCIENCE, AlternateSubcategory.ENGINEERING, - AlternateSubcategory.MISC_SCIENCE + AlternateSubcategory.MISC_SCIENCE, ]: - return Subcategory.OTHER_SCIENCE - + return (None, Subcategory.OTHER_SCIENCE) + if typed_alt_subcat in [ AlternateSubcategory.ARCHITECTURE, AlternateSubcategory.DANCE, @@ -118,26 +123,31 @@ def subcategory_correspondence(typed_alt_subcat: AlternateSubcategory) -> Subcat AlternateSubcategory.JAZZ, AlternateSubcategory.OPERA, AlternateSubcategory.PHOTOGRAPHY, - AlternateSubcategory.MISC_ARTS + AlternateSubcategory.MISC_ARTS, ]: - return Subcategory.OTHER_FINE_ARTS + return (None, Subcategory.OTHER_FINE_ARTS) if typed_alt_subcat in [ AlternateSubcategory.DRAMA, AlternateSubcategory.LONG_FICTION, AlternateSubcategory.POETRY, AlternateSubcategory.SHORT_FICTION, - AlternateSubcategory.MISC_LITERATURE + AlternateSubcategory.MISC_LITERATURE, ]: - return Subcategory.OTHER_LITERATURE + return (Category.LITERATURE, None) -def normalize_subcats( +def normalize_cats( + unnormalized_cats: UnnormalizedCategory, unnormalized_subcats: UnnormalizedSubcategory, - unnormalized_alt_subcats: UnnormalizedAlternateSubcategory -) -> Tuple[Subcategory, AlternateSubcategory]: - """Normalize a single or list of subcategories to a comma separated string.""" - + unnormalized_alt_subcats: UnnormalizedAlternateSubcategory, +) -> Tuple[Category, Subcategory, AlternateSubcategory]: + """ + Normalize a single or list of categories, subcategories, and alternate_subcategories + to their corresponding comma-separated strings, taking into account categories and + subcategories that must be added for the alternate_subcategories to work. + """ + typed_alt_subcats: list[AlternateSubcategory] = [] if isinstance(unnormalized_alt_subcats, str): @@ -145,24 +155,42 @@ def normalize_subcats( elif isinstance(unnormalized_alt_subcats, Iterable): for alt_subcat in unnormalized_alt_subcats: typed_alt_subcats.append(AlternateSubcategory(alt_subcat)) - + to_be_pushed_cats: list[Category] = [] to_be_pushed_subcats: list[Subcategory] = [] for alt_subcat in typed_alt_subcats: - to_be_pushed_subcats.append(subcategory_correspondence(alt_subcat)) - + cat, subcat = category_correspondence(alt_subcat) + if cat: + to_be_pushed_cats.append(cat) + if subcat: + to_be_pushed_subcats.append(subcat) + + final_cats = [] + if unnormalized_cats is None: + final_cats = to_be_pushed_cats + elif isinstance(unnormalized_cats, str): + final_cats = [Category(unnormalized_cats), *to_be_pushed_cats] + elif isinstance(unnormalized_cats, Iterable): + for subcat in unnormalized_cats: + final_cats.append(Subcategory(subcat)) + final_cats.append(*to_be_pushed_cats) + final_subcats = [] if unnormalized_subcats is None: final_subcats = to_be_pushed_subcats elif isinstance(unnormalized_subcats, str): - final_subcats = [Subcategory(unnormalized_alt_subcats), *to_be_pushed_subcats] + final_subcats = [Subcategory(unnormalized_subcats), *to_be_pushed_subcats] elif isinstance(unnormalized_subcats, Iterable): - for alt_subcat in unnormalized_subcats: - final_subcats.append(Subcategory(alt_subcat)) + for subcat in unnormalized_subcats: + final_subcats.append(Subcategory(subcat)) final_subcats.append(*to_be_pushed_subcats) - - return (normalize_enumlike(final_subcats, Subcategory), normalize_enumlike(typed_alt_subcats, AlternateSubcategory)) + + return ( + normalize_enumlike(final_cats, Category), + normalize_enumlike(final_subcats, Subcategory), + normalize_enumlike(typed_alt_subcats, AlternateSubcategory), + ) def prune_none(params: dict) -> dict: diff --git a/qbreader/asynchronous.py b/qbreader/asynchronous.py index 1b2d8eb..e91571a 100644 --- a/qbreader/asynchronous.py +++ b/qbreader/asynchronous.py @@ -185,6 +185,8 @@ class type. url = BASE_URL + "/query" + (normalized_categories, normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_cats(categories, subcategories, alternate_subcategories) + data = { "questionType": questionType, "searchType": searchType, @@ -196,9 +198,9 @@ class type. "randomize": api_utils.normalize_bool(randomize), "setName": setName, "difficulties": api_utils.normalize_diff(difficulties), - "categories": api_utils.normalize_cat(categories), - "subcategories": api_utils.normalize_subcat(subcategories), - "alternateSubcategories": api_utils.normalize_alt_subcat(alternate_subcategories), + "categories": normalized_categories, + "subcategories": normalized_subcategories, + "alternateSubcategories": normalized_alternate_subcategories, "maxReturnLength": maxReturnLength, "tossupPagination": tossupPagination, "bonusPagination": bonusPagination, @@ -270,11 +272,11 @@ async def random_tossup( url = BASE_URL + "/random-tossup" - (normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_subcats(subcategories, alternate_subcategories) + (normalized_categories, normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_cats(categories, subcategories, alternate_subcategories) data = { "difficulties": api_utils.normalize_diff(difficulties), - "categories": api_utils.normalize_cat(categories), + "categories": normalized_categories, "subcategories": normalized_subcategories, "alternateSubcategories": normalized_alternate_subcategories, "number": number, @@ -357,11 +359,11 @@ async def random_bonus( url = BASE_URL + "/random-bonus" - (normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_subcats(subcategories, alternate_subcategories) + (normalized_categories, normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_cats(categories, subcategories, alternate_subcategories) data = { "difficulties": api_utils.normalize_diff(difficulties), - "categories": api_utils.normalize_cat(categories), + "categories": normalized_categories, "subcategories": normalized_subcategories, "alternateSubcategories": normalized_alternate_subcategories, "number": number, diff --git a/qbreader/synchronous.py b/qbreader/synchronous.py index 134f6fe..240e920 100644 --- a/qbreader/synchronous.py +++ b/qbreader/synchronous.py @@ -150,7 +150,7 @@ class type. url = BASE_URL + "/query" - (normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_subcats(subcategories, alternate_subcategories) + (normalized_categories, normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_cats(categories, subcategories, alternate_subcategories) data = { "questionType": questionType, @@ -163,7 +163,7 @@ class type. "randomize": api_utils.normalize_bool(randomize), "setName": setName, "difficulties": api_utils.normalize_diff(difficulties), - "categories": api_utils.normalize_cat(categories), + "categories": normalized_categories, "subcategories": normalized_subcategories, "alternateSubcategories": normalized_alternate_subcategories, "maxReturnLength": maxReturnLength, @@ -237,11 +237,11 @@ def random_tossup( url = BASE_URL + "/random-tossup" - (normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_subcats(subcategories, alternate_subcategories) + (normalized_categories, normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_cats(categories, subcategories, alternate_subcategories) data = { "difficulties": api_utils.normalize_diff(difficulties), - "categories": api_utils.normalize_cat(categories), + "categories": normalized_categories, "subcategories": normalized_subcategories, "alternateSubcategories": normalized_alternate_subcategories, "number": number, @@ -324,10 +324,11 @@ def random_bonus( url = BASE_URL + "/random-bonus" - (normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_subcats(subcategories, alternate_subcategories) + (normalized_categories, normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_cats(categories, subcategories, alternate_subcategories) + data = { "difficulties": api_utils.normalize_diff(difficulties), - "categories": api_utils.normalize_cat(categories), + "categories": normalized_categories, "subcategories": normalized_subcategories, "alternateSubcategories": normalized_alternate_subcategories, "number": number, diff --git a/qbreader/types.py b/qbreader/types.py index 7f6ee93..0dfd9d2 100644 --- a/qbreader/types.py +++ b/qbreader/types.py @@ -270,10 +270,10 @@ def __init__( difficulty: Difficulty, category: Category, subcategory: Subcategory, - alternate_subcategory: AlternateSubcategory, packet: PacketMetadata, set: SetMetadata, number: int, + alternate_subcategory: Optional[AlternateSubcategory] = None, ): self.question: str = question self.question_sanitized: str = question_sanitized @@ -282,10 +282,10 @@ def __init__( self.difficulty: Difficulty = difficulty self.category: Category = category self.subcategory: Subcategory = subcategory - self.alternate_subcategory: AlternateSubcategory = alternate_subcategory self.packet: PacketMetadata = packet self.set: SetMetadata = set self.number: int = number + self.alternate_subcategory: AlternateSubcategory = alternate_subcategory @classmethod def from_json(cls: Type[Self], json: dict[str, Any]) -> Self: @@ -293,6 +293,7 @@ def from_json(cls: Type[Self], json: dict[str, Any]) -> Self: See https://www.qbreader.org/api-docs/schemas#tossups for schema. """ + alternate_subcategory = json.get("alternate_subcategory", None) return cls( question=json["question"], question_sanitized=json["question_sanitized"], @@ -301,10 +302,10 @@ def from_json(cls: Type[Self], json: dict[str, Any]) -> Self: difficulty=Difficulty(str(json["difficulty"])), category=Category(json["category"]), subcategory=Subcategory(json["subcategory"]), - alternate_subcategory=AlternateSubcategory(json["alternate_subcategory"]), packet=PacketMetadata.from_json(json["packet"]), set=SetMetadata.from_json(json["set"]), number=json["number"], + alternate_subcategory=AlternateSubcategory(alternate_subcategory) if alternate_subcategory else None, ) def check_answer_sync(self, givenAnswer: str) -> AnswerJudgement: @@ -357,10 +358,10 @@ def __init__( difficulty: Difficulty, category: Category, subcategory: Subcategory, - alternate_subcategory: AlternateSubcategory, set: SetMetadata, packet: PacketMetadata, number: int, + alternate_subcategory: Optional[AlternateSubcategory] = None, values: Optional[Sequence[int]] = None, difficultyModifiers: Optional[Sequence[DifficultyModifier]] = None, ): @@ -373,10 +374,10 @@ def __init__( self.difficulty: Difficulty = difficulty self.category: Category = category self.subcategory: Subcategory = subcategory - self.alternate_subcategory: AlternateSubcategory = alternate_subcategory self.set: SetMetadata = set self.packet: PacketMetadata = packet self.number: int = number + self.alternate_subcategory: AlternateSubcategory = alternate_subcategory self.values: Optional[tuple[int, ...]] = tuple(values) if values else None self.difficultyModifiers: Optional[tuple[DifficultyModifier, ...]] = ( tuple(difficultyModifiers) if difficultyModifiers else None @@ -388,6 +389,7 @@ def from_json(cls: Type[Self], json: dict[str, Any]) -> Self: See https://www.qbreader.org/api-docs/schemas#bonus for schema. """ + alternate_subcategory = json.get("alternate_subcategory", None) return cls( leadin=json["leadin"], leadin_sanitized=json["leadin_sanitized"], @@ -398,10 +400,10 @@ def from_json(cls: Type[Self], json: dict[str, Any]) -> Self: difficulty=Difficulty(str(json["difficulty"])), category=Category(json["category"]), subcategory=Subcategory(json["subcategory"]), - alternate_subcategory=AlternateSubcategory(json["alternate_subcategory"]), set=SetMetadata.from_json(json["set"]), packet=PacketMetadata.from_json(json["packet"]), number=json["number"], + alternate_subcategory=AlternateSubcategory(alternate_subcategory) if alternate_subcategory else None, values=json.get("values", None), difficultyModifiers=json.get("difficultyModifiers", None), ) From c0c19707fd90a05ae657865365e7543cc5114c5d Mon Sep 17 00:00:00 2001 From: Geoffrey Wu Date: Fri, 17 Jan 2025 15:02:35 -0600 Subject: [PATCH 7/9] fix missing social science logic --- qbreader/_api_utils.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/qbreader/_api_utils.py b/qbreader/_api_utils.py index 5b64b06..867205c 100644 --- a/qbreader/_api_utils.py +++ b/qbreader/_api_utils.py @@ -127,6 +127,16 @@ def category_correspondence( ]: return (None, Subcategory.OTHER_FINE_ARTS) + if typed_alt_subcat in [ + AlternateSubcategory.ANTHROPOLOGY, + AlternateSubcategory.ECONOMICS, + AlternateSubcategory.LINGUISTICS, + AlternateSubcategory.PSYCHOLOGY, + AlternateSubcategory.SOCIOLOGY, + AlternateSubcategory.OTHER_SOCIAL_SCIENCE, + ]: + return (None, Subcategory.SOCIAL_SCIENCE) + if typed_alt_subcat in [ AlternateSubcategory.DRAMA, AlternateSubcategory.LONG_FICTION, From 8c7886bb1a9a912f9461806151fd03a09df9557a Mon Sep 17 00:00:00 2001 From: Geoffrey Wu Date: Fri, 17 Jan 2025 15:03:00 -0600 Subject: [PATCH 8/9] add beliefs and practices --- qbreader/types.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qbreader/types.py b/qbreader/types.py index 0dfd9d2..d917a68 100644 --- a/qbreader/types.py +++ b/qbreader/types.py @@ -98,6 +98,9 @@ class AlternateSubcategory(enum.StrEnum): SOCIOLOGY = "Sociology" OTHER_SOCIAL_SCIENCE = "Other Social Science" + BELIEFS = "Beliefs" + PRACTICES = "Practices" + class Difficulty(enum.StrEnum): """Question difficulty enum.""" From d66038f777e596928392176f9d59624760a63c26 Mon Sep 17 00:00:00 2001 From: Geoffrey Wu Date: Fri, 17 Jan 2025 15:04:33 -0600 Subject: [PATCH 9/9] run linter --- qbreader/_api_utils.py | 1 + qbreader/asynchronous.py | 18 +++++++++++++++--- qbreader/synchronous.py | 18 +++++++++++++++--- qbreader/types.py | 12 +++++++++--- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/qbreader/_api_utils.py b/qbreader/_api_utils.py index 867205c..23ab000 100644 --- a/qbreader/_api_utils.py +++ b/qbreader/_api_utils.py @@ -93,6 +93,7 @@ def normalize_diff(unnormalized_diffs: UnnormalizedDifficulty): """Normalize a single or list of difficulty values to a comma separated string.""" return normalize_enumlike(unnormalized_diffs, Difficulty) + def normalize_cat(unnormalized_cats: UnnormalizedCategory): """Normalize a single or list of categories to a comma separated string.""" return normalize_enumlike(unnormalized_cats, Category) diff --git a/qbreader/asynchronous.py b/qbreader/asynchronous.py index e91571a..414b184 100644 --- a/qbreader/asynchronous.py +++ b/qbreader/asynchronous.py @@ -185,7 +185,11 @@ class type. url = BASE_URL + "/query" - (normalized_categories, normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_cats(categories, subcategories, alternate_subcategories) + ( + normalized_categories, + normalized_subcategories, + normalized_alternate_subcategories, + ) = api_utils.normalize_cats(categories, subcategories, alternate_subcategories) data = { "questionType": questionType, @@ -272,7 +276,11 @@ async def random_tossup( url = BASE_URL + "/random-tossup" - (normalized_categories, normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_cats(categories, subcategories, alternate_subcategories) + ( + normalized_categories, + normalized_subcategories, + normalized_alternate_subcategories, + ) = api_utils.normalize_cats(categories, subcategories, alternate_subcategories) data = { "difficulties": api_utils.normalize_diff(difficulties), @@ -359,7 +367,11 @@ async def random_bonus( url = BASE_URL + "/random-bonus" - (normalized_categories, normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_cats(categories, subcategories, alternate_subcategories) + ( + normalized_categories, + normalized_subcategories, + normalized_alternate_subcategories, + ) = api_utils.normalize_cats(categories, subcategories, alternate_subcategories) data = { "difficulties": api_utils.normalize_diff(difficulties), diff --git a/qbreader/synchronous.py b/qbreader/synchronous.py index 240e920..217528d 100644 --- a/qbreader/synchronous.py +++ b/qbreader/synchronous.py @@ -150,7 +150,11 @@ class type. url = BASE_URL + "/query" - (normalized_categories, normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_cats(categories, subcategories, alternate_subcategories) + ( + normalized_categories, + normalized_subcategories, + normalized_alternate_subcategories, + ) = api_utils.normalize_cats(categories, subcategories, alternate_subcategories) data = { "questionType": questionType, @@ -237,7 +241,11 @@ def random_tossup( url = BASE_URL + "/random-tossup" - (normalized_categories, normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_cats(categories, subcategories, alternate_subcategories) + ( + normalized_categories, + normalized_subcategories, + normalized_alternate_subcategories, + ) = api_utils.normalize_cats(categories, subcategories, alternate_subcategories) data = { "difficulties": api_utils.normalize_diff(difficulties), @@ -324,7 +332,11 @@ def random_bonus( url = BASE_URL + "/random-bonus" - (normalized_categories, normalized_subcategories, normalized_alternate_subcategories) = api_utils.normalize_cats(categories, subcategories, alternate_subcategories) + ( + normalized_categories, + normalized_subcategories, + normalized_alternate_subcategories, + ) = api_utils.normalize_cats(categories, subcategories, alternate_subcategories) data = { "difficulties": api_utils.normalize_diff(difficulties), diff --git a/qbreader/types.py b/qbreader/types.py index d917a68..c90fdb3 100644 --- a/qbreader/types.py +++ b/qbreader/types.py @@ -67,6 +67,7 @@ class Subcategory(enum.StrEnum): AUDITORY_FINE_ARTS = "Auditory Fine Arts" OTHER_FINE_ARTS = "Other Fine Arts" + class AlternateSubcategory(enum.StrEnum): """Question alternate subcategory enum.""" @@ -101,6 +102,7 @@ class AlternateSubcategory(enum.StrEnum): BELIEFS = "Beliefs" PRACTICES = "Practices" + class Difficulty(enum.StrEnum): """Question difficulty enum.""" @@ -308,7 +310,9 @@ def from_json(cls: Type[Self], json: dict[str, Any]) -> Self: packet=PacketMetadata.from_json(json["packet"]), set=SetMetadata.from_json(json["set"]), number=json["number"], - alternate_subcategory=AlternateSubcategory(alternate_subcategory) if alternate_subcategory else None, + alternate_subcategory=AlternateSubcategory(alternate_subcategory) + if alternate_subcategory + else None, ) def check_answer_sync(self, givenAnswer: str) -> AnswerJudgement: @@ -406,7 +410,9 @@ def from_json(cls: Type[Self], json: dict[str, Any]) -> Self: set=SetMetadata.from_json(json["set"]), packet=PacketMetadata.from_json(json["packet"]), number=json["number"], - alternate_subcategory=AlternateSubcategory(alternate_subcategory) if alternate_subcategory else None, + alternate_subcategory=AlternateSubcategory(alternate_subcategory) + if alternate_subcategory + else None, values=json.get("values", None), difficultyModifiers=json.get("difficultyModifiers", None), ) @@ -702,5 +708,5 @@ def __str__(self) -> str: "UnnormalizedDifficulty", "UnnormalizedCategory", "UnnormalizedSubcategory", - "UnnormalizedAlternateSubcategory" + "UnnormalizedAlternateSubcategory", )