Skip to content

Commit b15387b

Browse files
committed
added category search and set stream info
1 parent 2d307ce commit b15387b

File tree

6 files changed

+482
-13
lines changed

6 files changed

+482
-13
lines changed

kick/categories.py

Lines changed: 258 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,174 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Any
3+
from typing import TYPE_CHECKING
44

55
from .assets import Asset
6-
from .object import HTTPDataclass
6+
from .object import HTTPDataclass, BaseDataclass
77
from .utils import cached_property
88

99
if TYPE_CHECKING:
1010
from .types.categories import Category as CategoryPayload
1111
from .types.categories import InnerCategory as ParentCategoryPayload
12+
from .types.categories import (
13+
Category as CategoryPayload,
14+
InnerCategory as ParentCategoryPayload,
15+
CategoryDocument,
16+
CategorySearchResponse,
17+
TextHighlight as TextHighlightPayload,
18+
SearchHighlight as SearchHighlightPayload,
19+
TextMatchInfo as TextMatchInfoPayload,
20+
CategorySearchHit as CategorySearchHitPayload,
21+
)
22+
23+
__all__ = ("Category", "ParentCategory", "SearchCategory",
24+
"CategorySearchResult", "TextHighlight", "SearchHighlight",
25+
"TextMatchInfo", "CategorySearchHit")
26+
27+
28+
class TextHighlight(BaseDataclass["TextHighlightPayload"]):
29+
"""
30+
A dataclass representing text highlighting information
31+
Attributes
32+
-----------
33+
matched_tokens: list[str]
34+
List of tokens that matched
35+
snippet: str
36+
The highlighted text snippet
37+
"""
38+
39+
@property
40+
def matched_tokens(self) -> list[str]:
41+
return self._data["matched_tokens"]
42+
43+
@property
44+
def snippet(self) -> str:
45+
return self._data["snippet"]
46+
47+
def __repr__(self) -> str:
48+
return f"<TextHighlight matched_tokens={self.matched_tokens!r} snippet={self.snippet!r}>"
49+
50+
51+
class SearchHighlight(BaseDataclass["SearchHighlightPayload"]):
52+
"""
53+
A dataclass representing search highlight information
54+
Attributes
55+
-----------
56+
field: str
57+
The field that was highlighted
58+
matched_tokens: list[str]
59+
List of tokens that matched
60+
snippet: str
61+
The highlighted text snippet
62+
"""
63+
64+
@property
65+
def field(self) -> str:
66+
return self._data["field"]
67+
68+
@property
69+
def matched_tokens(self) -> list[str]:
70+
return self._data["matched_tokens"]
71+
72+
@property
73+
def snippet(self) -> str:
74+
return self._data["snippet"]
75+
76+
def __repr__(self) -> str:
77+
return f"<SearchHighlight field={self.field!r} matched_tokens={self.matched_tokens!r} snippet={self.snippet!r}>"
78+
79+
80+
class TextMatchInfo(BaseDataclass["TextMatchInfoPayload"]):
81+
"""
82+
A dataclass representing text match scoring information
83+
Attributes
84+
-----------
85+
best_field_score: str
86+
Score of the best matching field
87+
best_field_weight: int
88+
Weight of the best matching field
89+
fields_matched: int
90+
Number of fields that matched
91+
num_tokens_dropped: int
92+
Number of tokens that were dropped
93+
score: str
94+
Overall match score
95+
tokens_matched: int
96+
Number of tokens that matched
97+
typo_prefix_score: int
98+
Score for typo/prefix matches
99+
"""
100+
101+
@property
102+
def best_field_score(self) -> str:
103+
return self._data["best_field_score"]
104+
105+
@property
106+
def best_field_weight(self) -> int:
107+
return self._data["best_field_weight"]
108+
109+
@property
110+
def fields_matched(self) -> int:
111+
return self._data["fields_matched"]
112+
113+
@property
114+
def num_tokens_dropped(self) -> int:
115+
return self._data["num_tokens_dropped"]
116+
117+
@property
118+
def score(self) -> str:
119+
return self._data["score"]
120+
121+
@property
122+
def tokens_matched(self) -> int:
123+
return self._data["tokens_matched"]
124+
125+
@property
126+
def typo_prefix_score(self) -> int:
127+
return self._data["typo_prefix_score"]
128+
129+
def __repr__(self) -> str:
130+
return f"<TextMatchInfo score={self.score!r} tokens_matched={self.tokens_matched} fields_matched={self.fields_matched}>"
131+
132+
133+
class CategorySearchHit(BaseDataclass["CategorySearchHitPayload"]):
134+
"""
135+
A dataclass representing a category search hit result
136+
Attributes
137+
-----------
138+
document: SearchCategory
139+
The matching category document
140+
highlight: dict[str, TextHighlight]
141+
Highlights for each field
142+
highlights: list[SearchHighlight]
143+
List of all highlights
144+
text_match: int
145+
Text match score
146+
text_match_info: TextMatchInfo
147+
Detailed text match information
148+
"""
149+
150+
@cached_property
151+
def document(self) -> SearchCategory:
152+
return SearchCategory(data=self._data["document"])
153+
154+
@cached_property
155+
def highlight(self) -> dict[str, TextHighlight]:
156+
return {k: TextHighlight(data=v) for k, v in self._data["highlight"].items()}
12157

13-
__all__ = ("Category", "ParentCategory")
158+
@cached_property
159+
def highlights(self) -> list[SearchHighlight]:
160+
return [SearchHighlight(data=h) for h in self._data["highlights"]]
161+
162+
@property
163+
def text_match(self) -> int:
164+
return self._data["text_match"]
14165

166+
@cached_property
167+
def text_match_info(self) -> TextMatchInfo:
168+
return TextMatchInfo(data=self._data["text_match_info"])
169+
170+
def __repr__(self) -> str:
171+
return f"<CategorySearchHit document={self.document!r} text_match={self.text_match}>"
15172

16173
class ParentCategory(HTTPDataclass["ParentCategoryPayload"]):
17174
"""
@@ -67,6 +224,104 @@ def __eq__(self, other: object) -> bool:
67224
def __repr__(self) -> str:
68225
return f"<ParentCategory id={self.id!r} name={self.name!r} icon={self.icon!r}>"
69226

227+
class SearchCategory(BaseDataclass["CategoryDocument"]):
228+
"""
229+
A dataclass which represents a searchable category on kick
230+
Attributes
231+
-----------
232+
category_id: int
233+
The id of the parent category.
234+
id: str
235+
The id of the sub-category (game).
236+
name: str
237+
The category's name
238+
slug: str
239+
The category's slug
240+
description: str
241+
The category's description
242+
is_live: bool
243+
Whether the category is live
244+
is_mature: bool
245+
Whether the category is marked as mature
246+
src: str
247+
The category's banner image URL
248+
srcset: str
249+
The category's responsive image srcset
250+
parent: str
251+
The name of the parent category.
252+
"""
253+
254+
@property
255+
def category_id(self) -> int:
256+
return self._data["category_id"]
257+
258+
@property
259+
def id(self) -> str:
260+
return self._data["id"]
261+
262+
@property
263+
def name(self) -> str:
264+
return self._data["name"]
265+
266+
@property
267+
def slug(self) -> str:
268+
return self._data["slug"]
269+
270+
@property
271+
def description(self) -> str:
272+
return self._data["description"]
273+
274+
@property
275+
def is_live(self) -> bool:
276+
return self._data["is_live"]
277+
278+
@property
279+
def is_mature(self) -> bool:
280+
return self._data["is_mature"]
281+
282+
@property
283+
def src(self) -> str:
284+
return self._data["src"]
285+
286+
@property
287+
def srcset(self) -> str:
288+
return self._data["srcset"]
289+
290+
@property
291+
def parent(self) -> str:
292+
return self._data["parent"]
293+
294+
def __repr__(self) -> str:
295+
return f"<SearchCategory name={self.name!r} slug={self.slug!r} is_live={self.is_live}>"
296+
297+
298+
class CategorySearchResult(BaseDataclass["CategorySearchResponse"]):
299+
"""
300+
A dataclass which represents a category search response
301+
Attributes
302+
-----------
303+
found: int
304+
Total number of results found
305+
hits: list[SearchCategory]
306+
List of matching categories
307+
page: int
308+
Current page number
309+
"""
310+
311+
@property
312+
def found(self) -> int:
313+
return self._data["found"]
314+
315+
@property
316+
def page(self) -> int:
317+
return self._data["page"]
318+
319+
@cached_property
320+
def hits(self) -> list[CategorySearchHit]:
321+
return [CategorySearchHit(data=hit) for hit in self._data["hits"]]
322+
323+
def __repr__(self) -> str:
324+
return f"<CategorySearchResult found={self.found} page={self.page} hits={len(self.hits)}>"
70325

71326
class Category(HTTPDataclass["CategoryPayload"]):
72327
"""

kick/client.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
from .http import HTTPClient
1111
from .livestream import PartialLivestream
1212
from .message import Message
13-
from .users import ClientUser, PartialUser, User, DestinationInfo
13+
from .users import ClientUser, PartialUser, User, DestinationInfo, StreamInfo
14+
from .categories import CategorySearchResult
1415
from .utils import MISSING, decorator, setup_logging
1516

1617
if TYPE_CHECKING:
@@ -235,6 +236,63 @@ async def fetch_stream_url_and_key(self) -> DestinationInfo:
235236
data = await self.http.fetch_stream_destination_url_and_key()
236237
return DestinationInfo(data=data)
237238

239+
async def set_stream_info(
240+
self,
241+
title: str,
242+
language: str,
243+
subcategory_id: int,
244+
subcategory_name: str | None = None,
245+
is_mature: bool = False,
246+
) -> StreamInfo:
247+
"""
248+
|coro|
249+
Updates the stream information.
250+
Parameters
251+
-----------
252+
title: str
253+
The new stream title
254+
language: str
255+
The stream language (e.g. "English")
256+
subcategory_id: int
257+
The ID of the game/category
258+
subcategory_name: Optional[str]
259+
The name of the game/category (optional)
260+
is_mature: bool
261+
Whether the stream is marked as mature content
262+
Raises
263+
-----------
264+
HTTPException
265+
Failed to update stream information
266+
Returns
267+
-----------
268+
StreamInfo
269+
The stream info that was set to the logged in user's channel.
270+
"""
271+
272+
data = await self.http.set_stream_info(title, subcategory_name, subcategory_id, language, is_mature)
273+
return StreamInfo(data=data)
274+
275+
async def search_categories(self, query: str, /) -> CategorySearchResult:
276+
"""
277+
|coro|
278+
Searches for categories/games on Kick.
279+
Parameters
280+
-----------
281+
query: str
282+
The search query string
283+
Raises
284+
-----------
285+
HTTPException
286+
Search request failed
287+
Returns
288+
-----------
289+
SearchResponse
290+
The search results containing matching categories
291+
"""
292+
293+
data = await self.http.search_categories(query)
294+
return CategorySearchResult(data=data)
295+
238296
def dispatch(self, event_name: str, *args, **kwargs) -> None:
239297
event_name = f"on_{event_name}"
240298

0 commit comments

Comments
 (0)