Jikan4 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
jikan4/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from .aiojikan import AioJikan
2
+ from .jikan import Jikan
3
+ from jikan4.__version__ import __version__
4
+
5
+
6
+ __all__ = ["Jikan", "AioJikan", "__version__"]
jikan4/__version__.py ADDED
@@ -0,0 +1,3 @@
1
+ VERSION = (0, 3, 0)
2
+
3
+ __version__ = "0.3.0"
jikan4/aiojikan.py ADDED
@@ -0,0 +1,262 @@
1
+ from __future__ import annotations
2
+
3
+ import aiohttp
4
+
5
+ from .models import (
6
+ Anime,
7
+ AnimeSearch,
8
+ AnimeCharacters,
9
+ AnimeStaff,
10
+ AnimeEpisodes,
11
+ Episode,
12
+ AnimeNews,
13
+ AnimeForum,
14
+ )
15
+ from .utils.async_limiter import AsyncLimiter
16
+
17
+
18
+ class AioJikan:
19
+ """Async anime API wrapper with Tenrai as the default backend."""
20
+
21
+ def __init__(
22
+ self, base_url: str = "https://api.tenrai.org/v1", rate_limit: int = 60
23
+ ) -> None:
24
+ """Construct a AioJikan object
25
+
26
+ Args:
27
+ base_url (str, optional): Base URL for the anime API. Defaults to "https://api.tenrai.org/v1".
28
+ rate_limit (int, optional): Rate limit in requests per minute. Defaults to 60.
29
+
30
+ Returns:
31
+ AioJikan: AioJikan object
32
+
33
+ Examples:
34
+ >>> aiojikan = AioJikan()
35
+ >>> aiojikan = AioJikan("https://api.tenrai.org/v1")
36
+ """
37
+
38
+ base_url = base_url.rstrip("/")
39
+ self.base_url = base_url
40
+ self.session: aiohttp.ClientSession | None = None
41
+
42
+ if rate_limit:
43
+ self.rate_limiter = AsyncLimiter(calls_limit=rate_limit / 60, period=1)
44
+ self._get = self.rate_limiter.__call__(self._get)
45
+
46
+ async def close(self) -> None:
47
+ """Close the aiohttp session"""
48
+
49
+ if self.session is not None and not self.session.closed:
50
+ await self.session.close()
51
+
52
+ async def __aenter__(self) -> AioJikan:
53
+ return self
54
+
55
+ async def __aexit__(self, exc_type, exc, tb) -> None:
56
+ await self.close()
57
+
58
+ async def _get(self, endpoint: str, params: dict = None) -> dict:
59
+ """Make a GET request to the configured anime API
60
+
61
+ Args:
62
+ endpoint (str): Endpoint to request
63
+ params (dict, optional): Parameters to send with request. Defaults to None.
64
+
65
+ Returns:
66
+ dict: JSON response from the configured anime API
67
+ """
68
+
69
+ url = f"{self.base_url}/{endpoint}"
70
+ if self.session is None or self.session.closed:
71
+ self.session = aiohttp.ClientSession()
72
+
73
+ async with self.session.get(url, params=params) as response:
74
+ response.raise_for_status()
75
+ return await response.json()
76
+
77
+ async def get_anime(self, anime_id: int) -> Anime:
78
+ """Get anime information
79
+
80
+ Args:
81
+ anime_id (int): Anime ID
82
+
83
+ Returns:
84
+ Anime: Anime object
85
+
86
+ Examples:
87
+ >>> aiojikan = AioJikan()
88
+ >>> anime = await aiojikan.get_anime(1)
89
+ """
90
+
91
+ endpoint = f"anime/{anime_id}"
92
+ response = await self._get(endpoint)
93
+
94
+ return Anime(**response["data"])
95
+
96
+ async def get_anime_full(self, anime_id: int) -> Anime:
97
+ """Get anime information with full details
98
+
99
+ Args:
100
+ anime_id (int): Anime ID
101
+
102
+ Returns:
103
+ Anime: Anime object
104
+
105
+ Examples:
106
+ >>> aiojikan = AioJikan()
107
+ >>> anime = await aiojikan.get_anime_full(1)
108
+ """
109
+
110
+ endpoint = f"anime/{anime_id}/full"
111
+ response = await self._get(endpoint)
112
+
113
+ return Anime(**response["data"])
114
+
115
+ async def get_anime_characters(self, anime_id: int) -> AnimeCharacters:
116
+ """Get anime characters
117
+
118
+ Args:
119
+ anime_id (int): Anime ID
120
+
121
+ Returns:
122
+ AnimeCharacters: AnimeCharacters object
123
+
124
+ Examples:
125
+ >>> aiojikan = AioJikan()
126
+ >>> characters = await aiojikan.get_anime_characters(1)
127
+ """
128
+
129
+ endpoint = f"anime/{anime_id}/characters"
130
+ response = await self._get(endpoint)
131
+
132
+ return AnimeCharacters(**response)
133
+
134
+ async def get_anime_staff(self, anime_id: int) -> AnimeStaff:
135
+ """Get anime staff
136
+
137
+ Args:
138
+ anime_id (int): Anime ID
139
+
140
+ Returns:
141
+ AnimeStaff: AnimeStaff object
142
+
143
+ Examples:
144
+ >>> aiojikan = AioJikan()
145
+ >>> staff = await aiojikan.get_anime_staff(1)
146
+ """
147
+
148
+ endpoint = f"anime/{anime_id}/staff"
149
+ response = await self._get(endpoint)
150
+
151
+ return AnimeStaff(**response)
152
+
153
+ async def get_anime_episodes(self, anime_id: int) -> AnimeEpisodes:
154
+ """Get anime episodes
155
+
156
+ Args:
157
+ anime_id (int): Anime ID
158
+
159
+ Returns:
160
+ AnimeEpisodes: AnimeEpisodes object
161
+
162
+ Examples:
163
+ >>> aiojikan = AioJikan()
164
+ >>> episodes = await aiojikan.get_anime_episodes(1)
165
+ """
166
+
167
+ endpoint = f"anime/{anime_id}/episodes"
168
+ response = await self._get(endpoint)
169
+
170
+ return AnimeEpisodes(**response)
171
+
172
+ async def get_anime_episode(self, anime_id: int, episode: int) -> Episode:
173
+ """Get anime episode
174
+
175
+ Args:
176
+ anime_id (int): Anime ID
177
+ episode (int): Episode number
178
+
179
+ Returns:
180
+ AnimeEpisodes: AnimeEpisodes object
181
+
182
+ Examples:
183
+ >>> aiojikan = AioJikan()
184
+ >>> episode = await aiojikan.get_anime_episode(1, 1)
185
+ """
186
+
187
+ endpoint = f"anime/{anime_id}/episodes/{episode}"
188
+ response = await self._get(endpoint)
189
+
190
+ return Episode(**response["data"])
191
+
192
+ async def get_anime_news(self, anime_id: int, page: int = None) -> AnimeNews:
193
+ """Get anime news
194
+
195
+ Args:
196
+ anime_id (int): Anime ID
197
+ page (int, optional): Page number. Defaults to None.
198
+
199
+ Returns:
200
+ AnimeNews: AnimeNews object
201
+
202
+ Examples:
203
+ >>> aiojikan = AioJikan()
204
+ >>> news = await aiojikan.get_anime_news(1)
205
+ """
206
+
207
+ params = {}
208
+ if page:
209
+ params["page"] = page
210
+
211
+ endpoint = f"anime/{anime_id}/news"
212
+ response = await self._get(endpoint, params=params)
213
+
214
+ return AnimeNews(**response)
215
+
216
+ async def search_anime(
217
+ self, search_type: str, query: str, page: int = 1
218
+ ) -> AnimeSearch:
219
+ """Search for anime
220
+
221
+ Args:
222
+ search_type (str): Type of search to perform (tv, movie, ova, special, ona, music)
223
+ query (str): Query to search for
224
+ page (int, optional): Page number. Defaults to 1.
225
+
226
+ Returns:
227
+ AnimeSearch: AnimeSearch object
228
+
229
+ Examples:
230
+ >>> aiojikan = AioJikan()
231
+ >>> result = await aiojikan.search_anime("tv", "naruto")
232
+ """
233
+
234
+ endpoint = f"anime"
235
+ params = {"q": query, "page": page, "type": search_type}
236
+ response = await self._get(endpoint, params)
237
+
238
+ return AnimeSearch(**response)
239
+
240
+ async def get_anime_forum(self, anime_id: int, filter: str = None) -> AnimeForum:
241
+ """Get anime forum
242
+
243
+ Args:
244
+ anime_id (int): Anime ID
245
+ filter (str, optional): Filter to apply. ("all", "episode", "other"). Defaults to None.
246
+
247
+ Returns:
248
+ AnimeForum: AnimeForum object
249
+
250
+ Examples:
251
+ >>> aiojikan = AioJikan()
252
+ >>> forum = await aiojikan.get_anime_forum(1)
253
+ """
254
+
255
+ params = {}
256
+ if filter:
257
+ params["filter"] = filter
258
+
259
+ endpoint = f"anime/{anime_id}/forum"
260
+ response = await self._get(endpoint, params=params)
261
+
262
+ return AnimeForum(**response)
jikan4/jikan.py ADDED
@@ -0,0 +1,239 @@
1
+ import requests
2
+
3
+ from .models import (
4
+ Anime,
5
+ AnimeSearch,
6
+ AnimeCharacters,
7
+ AnimeStaff,
8
+ AnimeEpisodes,
9
+ Episode,
10
+ AnimeNews,
11
+ AnimeForum,
12
+ )
13
+ from .utils.limiter import Limiter
14
+
15
+
16
+ class Jikan:
17
+ """Anime API wrapper with Tenrai as the default backend."""
18
+
19
+ def __init__(
20
+ self, base_url: str = "https://api.tenrai.org/v1", rate_limit: int = 60
21
+ ):
22
+ """Construct a Jikan object
23
+
24
+ Args:
25
+ base_url (str, optional): Base URL for the anime API. Defaults to "https://api.tenrai.org/v1".
26
+ rate_limit (int, optional): Rate limit in requests per minute. Defaults to 60.
27
+
28
+ Returns:
29
+ Jikan: Jikan object
30
+
31
+ Examples:
32
+ >>> jikan = Jikan()
33
+ >>> jikan = Jikan("https://api.tenrai.org/v1")
34
+ """
35
+
36
+ base_url = base_url.rstrip("/")
37
+ self.base_url = base_url
38
+ self.session = requests.Session()
39
+
40
+ if rate_limit:
41
+ self.rate_limiter = Limiter(calls_limit=rate_limit, period=60, spread=True)
42
+ self._get = self.rate_limiter.__call__(self._get)
43
+
44
+ def _get(self, endpoint: str, params: dict = None) -> dict:
45
+ """Make a GET request to the configured anime API
46
+
47
+ Args:
48
+ endpoint (str): Endpoint to request
49
+ params (dict, optional): Parameters to send with request. Defaults to None.
50
+
51
+ Returns:
52
+ dict: JSON response from the configured anime API
53
+ """
54
+
55
+ url = f"{self.base_url}/{endpoint}"
56
+
57
+ response = self.session.get(url, params=params)
58
+
59
+ response.raise_for_status()
60
+ return response.json()
61
+
62
+ def get_anime(self, anime_id: int) -> Anime:
63
+ """Get anime information
64
+
65
+ Args:
66
+ anime_id (int): Anime ID
67
+
68
+ Returns:
69
+ Anime: Anime object
70
+
71
+ Examples:
72
+ >>> jikan = Jikan()
73
+ >>> anime = jikan.get_anime(1)
74
+ """
75
+
76
+ endpoint = f"anime/{anime_id}"
77
+ response = self._get(endpoint)
78
+ return Anime(**response["data"])
79
+
80
+ def get_anime_full(self, anime_id: int) -> Anime:
81
+ """Get anime information with full details
82
+
83
+ Args:
84
+ anime_id (int): Anime ID
85
+
86
+ Returns:
87
+ Anime: Anime object
88
+
89
+ Examples:
90
+ >>> jikan = Jikan()
91
+ >>> anime = jikan.get_anime_full(1)
92
+ """
93
+
94
+ endpoint = f"anime/{anime_id}/full"
95
+ response = self._get(endpoint)
96
+ return Anime(**response["data"])
97
+
98
+ def get_anime_characters(self, anime_id: int) -> AnimeCharacters:
99
+ """Get anime characters
100
+
101
+ Args:
102
+ anime_id (int): Anime ID
103
+
104
+ Returns:
105
+ AnimeCharacters: AnimeCharacters object
106
+
107
+ Examples:
108
+ >>> jikan = Jikan()
109
+ >>> characters = jikan.get_anime_characters(1)
110
+ """
111
+
112
+ endpoint = f"anime/{anime_id}/characters"
113
+ response = self._get(endpoint)
114
+ return AnimeCharacters(**response)
115
+
116
+ def get_anime_staff(self, anime_id: int) -> AnimeStaff:
117
+ """Get anime staff
118
+
119
+ Args:
120
+ anime_id (int): Anime ID
121
+
122
+ Returns:
123
+ AnimeStaff: AnimeStaff object
124
+
125
+ Examples:
126
+ >>> jikan = Jikan()
127
+ >>> staff = jikan.get_anime_staff(1)
128
+ """
129
+
130
+ endpoint = f"anime/{anime_id}/staff"
131
+ response = self._get(endpoint)
132
+ return AnimeStaff(**response)
133
+
134
+ def get_anime_episodes(self, anime_id: int) -> AnimeEpisodes:
135
+ """Get anime episodes
136
+
137
+ Args:
138
+ anime_id (int): Anime ID
139
+
140
+ Returns:
141
+ AnimeEpisodes: AnimeEpisodes object
142
+
143
+ Examples:
144
+ >>> jikan = Jikan()
145
+ >>> episodes = jikan.get_anime_episodes(1)
146
+ """
147
+
148
+ endpoint = f"anime/{anime_id}/episodes"
149
+ response = self._get(endpoint)
150
+ return AnimeEpisodes(**response)
151
+
152
+ def get_anime_episode(self, anime_id: int, episode: int) -> Episode:
153
+ """Get anime episode
154
+
155
+ Args:
156
+ anime_id (int): Anime ID
157
+ episode (int): Episode number
158
+
159
+ Returns:
160
+ Episode: Episode object
161
+
162
+ Examples:
163
+ >>> jikan = Jikan()
164
+ >>> episode = jikan.get_anime_episode(1, 1)
165
+ """
166
+
167
+ endpoint = f"anime/{anime_id}/episodes/{episode}"
168
+ response = self._get(endpoint)
169
+ return Episode(**response["data"])
170
+
171
+ def get_anime_news(self, anime_id: int, page: int = None) -> AnimeNews:
172
+ """Get anime news
173
+
174
+ Args:
175
+ anime_id (int): Anime ID
176
+ page (int, optional): Page number. Defaults to None.
177
+
178
+ Returns:
179
+ AnimeNews: AnimeNews object
180
+
181
+ Examples:
182
+ >>> jikan = Jikan()
183
+ >>> news = jikan.get_anime_news(1)
184
+ """
185
+
186
+ params = {}
187
+ if page:
188
+ params["page"] = page
189
+
190
+ endpoint = f"anime/{anime_id}/news"
191
+ response = self._get(endpoint, params=params)
192
+
193
+ return AnimeNews(**response)
194
+
195
+ def search_anime(self, search_type: str, query: str, page: int = 1) -> AnimeSearch:
196
+ """Search for anime
197
+
198
+ Args:
199
+ search_type (str): Type of search to perform (tv, movie, ova, special, ona, music)
200
+ query (str): Query to search for
201
+ page (int, optional): Page number. Defaults to 1.
202
+
203
+ Returns:
204
+ AnimeSearch: AnimeSearch object
205
+
206
+ Examples:
207
+ >>> jikan = Jikan()
208
+ >>> result = jikan.search_anime("tv", "naruto")
209
+ """
210
+
211
+ endpoint = f"anime"
212
+ params = {"q": query, "page": page, "type": search_type}
213
+ response = self._get(endpoint, params)
214
+
215
+ return AnimeSearch(**response)
216
+
217
+ def get_anime_forum(self, anime_id: int, filter: str = None) -> AnimeForum:
218
+ """Get anime forum
219
+
220
+ Args:
221
+ anime_id (int): Anime ID
222
+ filter (str, optional): Filter to apply. ("all", "episode", "other"). Defaults to None.
223
+
224
+ Returns:
225
+ AnimeForum: AnimeForum object
226
+
227
+ Examples:
228
+ >>> jikan = Jikan()
229
+ >>> forum = jikan.get_anime_forum(1)
230
+ """
231
+
232
+ params = {}
233
+ if filter:
234
+ params["filter"] = filter
235
+
236
+ endpoint = f"anime/{anime_id}/forum"
237
+ response = self._get(endpoint, params=params)
238
+
239
+ return AnimeForum(**response)
jikan4/models.py ADDED
@@ -0,0 +1,291 @@
1
+ from typing import List
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ class Date(BaseModel):
6
+ day: int | None = None
7
+ month: int | None = None
8
+ year: int | None = None
9
+
10
+
11
+ class JPG(BaseModel):
12
+ image_url: str | None = None
13
+ small_image_url: str | None = None
14
+ large_image_url: str | None = None
15
+
16
+
17
+ class Webp(BaseModel):
18
+ image_url: str | None = None
19
+ small_image_url: str | None = None
20
+ large_image_url: str | None = None
21
+
22
+
23
+ class Image(BaseModel):
24
+ jpg: JPG = JPG()
25
+ webp: Webp = Webp()
26
+
27
+
28
+ class Trailer(BaseModel):
29
+ youtube_id: str | None = None
30
+ url: str | None = None
31
+ embed_url: str | None = None
32
+
33
+
34
+ class Title(BaseModel):
35
+ type: str | None = None
36
+ title: str | None = None
37
+
38
+
39
+ class Prop(BaseModel):
40
+ from_: Date = Field(default_factory=Date, alias="from")
41
+ to: Date = Date()
42
+ string: str | None = None
43
+
44
+
45
+ class AirDate(BaseModel):
46
+ from_: str | None = Field(None, alias="from")
47
+ to: str | None = None
48
+ prop: Prop = Prop()
49
+
50
+
51
+ class Broadcast(BaseModel):
52
+ day: str | None = None
53
+ time: str | None = None
54
+ timezone: str | None = None
55
+ string: str | None = None
56
+
57
+
58
+ class Producer(BaseModel):
59
+ mal_id: int | None = None
60
+ type: str | None = None
61
+ name: str | None = None
62
+ url: str | None = None
63
+
64
+
65
+ class Licenser(BaseModel):
66
+ mal_id: int | None = None
67
+ type: str | None = None
68
+ name: str | None = None
69
+ url: str | None = None
70
+
71
+
72
+ class Studio(BaseModel):
73
+ mal_id: int | None = None
74
+ type: str | None = None
75
+ name: str | None = None
76
+ url: str | None = None
77
+
78
+
79
+ class Streaming(BaseModel):
80
+ name: str | None = None
81
+ url: str | None = None
82
+
83
+
84
+ class Genre(BaseModel):
85
+ mal_id: int | None = None
86
+ type: str | None = None
87
+ name: str | None = None
88
+ url: str | None = None
89
+
90
+
91
+ class ExplicitGenre(BaseModel):
92
+ mal_id: int | None = None
93
+ type: str | None = None
94
+ name: str | None = None
95
+ url: str | None = None
96
+
97
+
98
+ class ThemeGenre(BaseModel):
99
+ mal_id: int | None = None
100
+ type: str | None = None
101
+ name: str | None = None
102
+ url: str | None = None
103
+
104
+
105
+ class Demographic(BaseModel):
106
+ mal_id: int | None = None
107
+ type: str | None = None
108
+ name: str | None = None
109
+ url: str | None = None
110
+
111
+
112
+ class Entry(BaseModel):
113
+ mal_id: int | None = None
114
+ type: str | None = None
115
+ name: str | None = None
116
+ url: str | None = None
117
+
118
+
119
+ class Related(BaseModel):
120
+ relation: str | None = None
121
+ entry: List[Entry] = []
122
+
123
+
124
+ class Theme(BaseModel):
125
+ openings: List[str] = []
126
+ endings: List[str] = []
127
+
128
+
129
+ class External(BaseModel):
130
+ name: str | None = None
131
+ url: str | None = None
132
+
133
+
134
+ class Items(BaseModel):
135
+ count: int = 0
136
+ total: int = 0
137
+ per_page: int = 0
138
+
139
+
140
+ class Pagination(BaseModel):
141
+ last_visible_page: int = 1
142
+ has_next_page: bool = False
143
+ current_page: int = 1
144
+ items: Items = Items()
145
+
146
+
147
+ class Character(BaseModel):
148
+ mal_id: int | None = None
149
+ url: str | None = None
150
+ images: Image = Image()
151
+ name: str | None = None
152
+
153
+
154
+ class Person(BaseModel):
155
+ mal_id: int | None = None
156
+ url: str | None = None
157
+ images: Image = Image()
158
+ name: str | None = None
159
+
160
+
161
+ class VoiceActor(BaseModel):
162
+ person: Person = Person()
163
+ language: str | None = None
164
+
165
+
166
+ class Staff(BaseModel):
167
+ person: Person = Person()
168
+ positions: List[str] = []
169
+
170
+
171
+ class News(BaseModel):
172
+ mal_id: int | None = None
173
+ url: str | None = None
174
+ title: str | None = None
175
+ date: str | None = None
176
+ author_username: str | None = None
177
+ author_url: str | None = None
178
+ forum_url: str | None = None
179
+ images: Image = Image()
180
+ comments: int = 0
181
+ excerpt: str | None = None
182
+
183
+
184
+ class Episode(BaseModel):
185
+ mal_id: int | None = None
186
+ url: str | None = None
187
+ title: str | None = None
188
+ title_japanese: str | None = None
189
+ title_romanji: str | None = None
190
+ duration: float | None = None
191
+ aired: str | None = None
192
+ filler: bool = False
193
+ recap: bool = False
194
+ synopsis: str | None = None
195
+ forum_url: str | None = None
196
+
197
+
198
+ class Anime(BaseModel):
199
+ mal_id: int
200
+ url: str
201
+ images: Image = Image()
202
+ trailer: Trailer = Trailer()
203
+ approved: bool | None = None
204
+ titles: List[Title] = []
205
+ title: str | None = None
206
+ title_english: str | None = None
207
+ title_japanese: str | None = None
208
+ title_synonyms: List[str] = []
209
+ type: str | None = None
210
+ source: str | None = None
211
+ episodes: int | None = None
212
+ status: str | None = None
213
+ airing: bool | None = None
214
+ aired: AirDate = AirDate()
215
+ duration: str | None = None
216
+ rating: str | None = None
217
+ score: float | None = None
218
+ scored_by: int | None = None
219
+ rank: int | None = None
220
+ popularity: int | None = None
221
+ members: int | None = None
222
+ favorites: int | None = None
223
+ synopsis: str | None = None
224
+ background: str | None = None
225
+ season: str | None = None
226
+ year: int | None = None
227
+ broadcast: Broadcast = Broadcast()
228
+ producers: List[Producer] = []
229
+ licensors: List[Licenser] = []
230
+ studios: List[Studio] = []
231
+ genres: List[Genre] = []
232
+ explicit_genres: List[ExplicitGenre] = []
233
+ ThemeGenres: List[ThemeGenre] = []
234
+ demographics: List[Demographic] = []
235
+ relations: List[Related] = []
236
+ theme: Theme = Theme()
237
+ external: list[External] = []
238
+ streaming: List[Streaming] = []
239
+
240
+
241
+ class AnimeSearch(BaseModel):
242
+ data: List[Anime] = []
243
+ pagination: Pagination = Pagination()
244
+
245
+
246
+ class AnimeCharacter(BaseModel):
247
+ character: Character = Character()
248
+ role: str | None = None
249
+ voice_actors: List[VoiceActor] = []
250
+
251
+
252
+ class AnimeCharacters(BaseModel):
253
+ data: List[AnimeCharacter] = []
254
+ pagination: Pagination = Pagination()
255
+
256
+
257
+ class AnimeStaff(BaseModel):
258
+ data: List[Staff] = []
259
+ pagination: Pagination = Pagination()
260
+
261
+
262
+ class AnimeEpisodes(BaseModel):
263
+ data: List[Episode] = []
264
+ pagination: Pagination = Pagination()
265
+
266
+
267
+ class AnimeNews(BaseModel):
268
+ data: List[News] = []
269
+ pagination: Pagination = Pagination()
270
+
271
+
272
+ class Comment(BaseModel):
273
+ url: str
274
+ author_username: str
275
+ author_url: str
276
+ date: str
277
+
278
+
279
+ class Forum(BaseModel):
280
+ mal_id: int
281
+ url: str
282
+ title: str
283
+ date: str
284
+ author_username: str
285
+ author_url: str
286
+ comments: int
287
+ last_comment: Comment
288
+
289
+
290
+ class AnimeForum(BaseModel):
291
+ data: List[Forum] = []
File without changes
@@ -0,0 +1,33 @@
1
+ import asyncio
2
+ import time
3
+
4
+
5
+ class AsyncLimiter:
6
+ def __init__(self, calls_limit: int = 5, period: int = 1, spread: bool = False):
7
+ if spread:
8
+ self.calls_limit = 1
9
+ self.period = period / calls_limit
10
+ else:
11
+ self.calls_limit = calls_limit
12
+ self.period = period
13
+
14
+ self.semaphore = asyncio.Semaphore(calls_limit)
15
+ self.requests_finish_time = []
16
+
17
+ async def sleep(self):
18
+ if len(self.requests_finish_time) >= self.calls_limit:
19
+ sleep_before = self.requests_finish_time.pop(0)
20
+ if sleep_before >= time.monotonic():
21
+ await asyncio.sleep(sleep_before - time.monotonic())
22
+
23
+ def __call__(self, func):
24
+ async def wrapper(*args, **kwargs):
25
+
26
+ async with self.semaphore:
27
+ await self.sleep()
28
+ res = await func(*args, **kwargs)
29
+ self.requests_finish_time.append(time.monotonic() + self.period)
30
+
31
+ return res
32
+
33
+ return wrapper
@@ -0,0 +1,26 @@
1
+ import time
2
+
3
+
4
+ class Limiter:
5
+ def __init__(self, calls_limit: int = 5, period: int = 1, spread: bool = False):
6
+ if spread:
7
+ self.calls_limit = 1
8
+ self.period = period / calls_limit
9
+
10
+ else:
11
+ self.calls_limit = calls_limit
12
+ self.period = period
13
+
14
+ self.calls = -1 # To make the first call not wait
15
+
16
+ def __call__(self, func):
17
+ def wrapper(*args, **kwargs):
18
+ self.calls += 1
19
+ if self.calls >= self.calls_limit:
20
+ time.sleep(self.period)
21
+ self.calls = 0
22
+
23
+ res = func(*args, **kwargs)
24
+ return res
25
+
26
+ return wrapper
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.5
2
+ Name: Jikan4
3
+ Version: 0.3.0
4
+ Summary: A Python wrapper for an anime API, with Tenrai as the default backend
5
+ Project-URL: Documentation, https://github.com/mdabessa/jikan4#readme
6
+ Project-URL: Issues, https://github.com/mdabessa/jikan4/issues
7
+ Project-URL: Source, https://github.com/mdabessa/jikan4
8
+ Author-email: Matheus Bessa <mdabessa@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: aiohttp
17
+ Requires-Dist: pydantic
18
+ Requires-Dist: requests
19
+ Description-Content-Type: text/markdown
20
+
21
+ # jikan4
22
+ A Python wrapper for an anime API, with Tenrai as the default backend.
23
+
24
+
25
+ ## Tenrai Endpoint Support
26
+
27
+ The client currently uses these Tenrai v1 endpoints:
28
+
29
+ | Client method | Endpoint | Status |
30
+ | --- | --- | --- |
31
+ | `get_anime` | `GET /anime/{id}` | Supported |
32
+ | `get_anime_full` | `GET /anime/{id}/full` | Supported |
33
+ | `get_anime_characters` | `GET /anime/{id}/characters` | Supported |
34
+ | `get_anime_staff` | `GET /anime/{id}/staff` | Supported |
35
+ | `get_anime_episodes` | `GET /anime/{id}/episodes` | Supported |
36
+ | `get_anime_episode` | `GET /anime/{id}/episodes/{episode}` | Supported |
37
+ | `get_anime_news` | `GET /anime/{id}/news` | Supported |
38
+ | `search_anime` | `GET /anime` | Supported |
39
+ | `get_anime_forum` | `GET /anime/{id}/forum` | Supported |
40
+
41
+
42
+ ## Installation
43
+ ```bash
44
+ pip install jikan4
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ### Basic Usage
50
+ ```python
51
+ from jikan4.jikan import Jikan
52
+
53
+ jikan = Jikan()
54
+
55
+ anime = jikan.get_anime(1)
56
+ search = jikan.search_anime("tv", "naruto")
57
+ ```
58
+
59
+ ### Async Usage
60
+ ```python
61
+ import asyncio
62
+ from jikan4.aiojikan import AioJikan
63
+
64
+
65
+ async def main():
66
+ jikan = AioJikan()
67
+
68
+ anime = await jikan.get_anime(1)
69
+ search = await jikan.search_anime('tv', 'naruto')
70
+
71
+ jikan.close()
72
+
73
+
74
+ if __name__ == '__main__':
75
+ asyncio.run(main())
76
+ ```
@@ -0,0 +1,12 @@
1
+ jikan4/__init__.py,sha256=DkCdx02GtuXO-1MT8m9Z6fObEkqO9xuTYMGnNme9m3s,148
2
+ jikan4/__version__.py,sha256=xO9M0x5NcE4qkElhUdH5jpvqzB6ZzhoKWKVHMSEpIH8,43
3
+ jikan4/aiojikan.py,sha256=5rmJMvLzb3SSwnqhx5jE0Cgj2kmvzkxqSPzeupM5vU8,7167
4
+ jikan4/jikan.py,sha256=-0dkzlMq8BD-OGg-ZucLmmQfkXQJhvf7Z3pjXBLjfuY,6325
5
+ jikan4/models.py,sha256=dIQFm3sp-C36V86h5kEL6SlK_bv1tx8BCvf1QCAGHYI,6287
6
+ jikan4/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ jikan4/utils/async_limiter.py,sha256=r-vX1qbX7SXVBfxXQDvrTLyear9Vwk0qgV4553TPozE,1017
8
+ jikan4/utils/limiter.py,sha256=3qNhBCFSO6-kHDB-_6hwsxpoDcdSlCJ54165eIr7x5A,667
9
+ jikan4-0.3.0.dist-info/METADATA,sha256=_e7dR7e4d0o5q6AdB1oAS3lUvcJ4f_hUbBXmrKjQ_RA,2009
10
+ jikan4-0.3.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
11
+ jikan4-0.3.0.dist-info/licenses/LICENSE,sha256=SytWxP_4vwtLBqoMmwui2zB4QQ5nZXVhzy_GD2Opeuc,1070
12
+ jikan4-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Matheus Bessa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.