scurrypy 0.4__py3-none-any.whl → 0.4.1__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.

Potentially problematic release.


This version of scurrypy might be problematic. Click here for more details.

discord/client.py CHANGED
@@ -228,9 +228,9 @@ class Client(ClientLike):
228
228
  guild_id (int): id of the target guild
229
229
  """
230
230
  if self._guild_commands.get(guild_id):
231
- self._logger.log_info(f"Guild {guild_id} already queued, skipping clear.")
231
+ self._logger.log_warn(f"Guild {guild_id} already queued, skipping clear.")
232
232
  return
233
-
233
+
234
234
  self._guild_commands[guild_id] = []
235
235
 
236
236
  async def _listen(self):
@@ -268,7 +268,7 @@ class Client(ClientLike):
268
268
  self._ws.sequence = None
269
269
  raise ConnectionError("Invalid session.")
270
270
  case 11:
271
- self._logger.log_debug("Heartbeat ACK received")
271
+ self._logger.log_info("Heartbeat ACK received")
272
272
 
273
273
  except asyncio.CancelledError:
274
274
  break
@@ -283,14 +283,15 @@ class Client(ClientLike):
283
283
  raise
284
284
  except Exception as e:
285
285
  self._logger.log_error(f"{type(e).__name__} - {e}")
286
+ self._logger.log_traceback()
286
287
  continue
287
288
 
288
- async def start(self):
289
+ async def _start(self):
289
290
  """Runs the main lifecycle of the bot.
290
291
  Handles connection setup, heartbeat management, event loop, and automatic reconnects.
291
292
  """
292
293
  try:
293
- await self._http.start_session()
294
+ await self._http.start()
294
295
  await self._ws.connect()
295
296
  await self._ws.start_heartbeat()
296
297
 
@@ -350,7 +351,7 @@ class Client(ClientLike):
350
351
 
351
352
  # Close HTTP before gateway since it's more important
352
353
  self._logger.log_debug("Closing HTTP session...")
353
- await self._http.close_session()
354
+ await self._http.close()
354
355
 
355
356
  # Then try websocket with short timeout
356
357
  try:
@@ -365,11 +366,12 @@ class Client(ClientLike):
365
366
  setting up emojis and hooks, and then listens for gateway events.
366
367
  """
367
368
  try:
368
- asyncio.run(self.start())
369
+ asyncio.run(self._start())
369
370
  except KeyboardInterrupt:
370
371
  self._logger.log_debug("Shutdown requested via KeyboardInterrupt.")
371
372
  except Exception as e:
372
373
  self._logger.log_error(f"{type(e).__name__} {e}")
374
+ self._logger.log_traceback()
373
375
  finally:
374
376
  self._logger.log_high_priority("Bot shutting down.")
375
377
  self._logger.close()
@@ -64,7 +64,7 @@ class CommandDispatcher:
64
64
  await self._http.request(
65
65
  'PUT',
66
66
  f"applications/{self.application_id}/guilds/{guild_id}/commands",
67
- [command._to_dict() for command in cmds]
67
+ data=[command._to_dict() for command in cmds]
68
68
  )
69
69
 
70
70
  async def _register_global_commands(self, commands: list):
@@ -76,7 +76,7 @@ class CommandDispatcher:
76
76
 
77
77
  global_commands = [command._to_dict() for command in commands]
78
78
 
79
- await self._http.request('PUT', f"applications/{self.application_id}/commands", global_commands)
79
+ await self._http.request('PUT', f"applications/{self.application_id}/commands", data=global_commands)
80
80
 
81
81
  def command(self, name: str, handler):
82
82
  """Decorator to register slash commands.
@@ -161,3 +161,4 @@ class CommandDispatcher:
161
161
  self._logger.log_info(f"Interaction Event '{name}' Acknowledged.")
162
162
  except Exception as e:
163
163
  self._logger.log_error(f"Error in interaction '{name}': {e}")
164
+ self._logger.log_traceback()
@@ -43,7 +43,7 @@ class EventDispatcher:
43
43
  """HTTP session for requests."""
44
44
 
45
45
  self._logger = client._logger
46
- """HTTP session for requests"""
46
+ """Logger instance to log events."""
47
47
 
48
48
  self.config = client.config
49
49
  """User-defined bot config for persistent data."""
@@ -57,3 +57,8 @@ class PrefixDispatcher:
57
57
  self._logger.log_info(f"Prefix Event '{command}' Acknowledged.")
58
58
  except Exception as e:
59
59
  self._logger.log_error(f"Error in prefix command '{command}': {e}")
60
+
61
+ if self._logger.dev_mode:
62
+ import traceback
63
+ traceback.print_exc()
64
+ print("-----------------------------------\n")
discord/gateway.py CHANGED
@@ -67,7 +67,6 @@ class GatewayClient:
67
67
 
68
68
  if message:
69
69
  data: dict = json.loads(message)
70
- self._logger.log_debug(f"Received: {DISCORD_OP_CODES.get(data.get('op'))} - {json.dumps(data, indent=4)}")
71
70
  self._logger.log_info(f"Received: {DISCORD_OP_CODES.get(data.get('op'))}")
72
71
  return data
73
72
 
@@ -79,7 +78,6 @@ class GatewayClient:
79
78
  Args:
80
79
  message (dict): the message to send
81
80
  """
82
- self._logger.log_debug(f"Sending payload: {message}")
83
81
  await self.ws.send(json.dumps(message))
84
82
 
85
83
  async def send_heartbeat_loop(self):
@@ -90,8 +88,7 @@ class GatewayClient:
90
88
  await asyncio.sleep(self.heartbeat_interval / 1000)
91
89
  hb_data = {"op": 1, "d": self.sequence}
92
90
  await self.send(hb_data)
93
- self._logger.log_debug(f"Sending: {hb_data}")
94
- self._logger.log_info("Heartbeat sent.")
91
+ self._logger.log_debug(f"Sent HEARTBEAT: {hb_data}")
95
92
 
96
93
  async def identify(self):
97
94
  """Sends the IDENIFY payload (token, intents, connection properties).
@@ -111,7 +108,7 @@ class GatewayClient:
111
108
  }
112
109
  await self.send(i)
113
110
  log_i = self._logger.redact(i)
114
- self._logger.log_debug(f"Sending: {log_i}")
111
+ self._logger.log_debug(f"Sent IDENTIFY: {log_i}")
115
112
  self._logger.log_high_priority("Identify sent.")
116
113
 
117
114
  async def start_heartbeat(self):
discord/http.py CHANGED
@@ -1,280 +1,213 @@
1
1
  import aiohttp
2
2
  import aiofiles
3
3
  import asyncio
4
- import time
5
4
  import json
6
- import ssl
7
- from dataclasses import dataclass
8
- from urllib.parse import urlencode
5
+
6
+ from typing import Any, Optional
9
7
 
10
8
  from .logger import Logger
11
- from .error import DiscordError
12
-
13
- ssl_ctx = ssl.create_default_context()
14
- ssl_ctx.options |= ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 # Disable old SSL
15
-
16
- DISCORD_HTTP_CODES = {
17
- 200: "Successful Request",
18
- 201: "Successful Creation",
19
- 204: "No Content",
20
- 304: "Not Modified",
21
- 400: "Bad Request",
22
- 401: "Missing Authorization",
23
- 403: "Missing Permission",
24
- 404: "Resource Not Found",
25
- 405: "Invalid Method",
26
- 429: "Rate Limited",
27
- 502: "Gateway Unavailable"
28
- }
29
-
30
- @dataclass
31
- class RequestItem:
32
- """Data container representing an HTTP request to Discord's API.
33
- Used internally by HTTPClient for queuing and processing requests.
34
- """
35
- method: str
36
- """HTTP method (e.g., GET, POST, DELETE, PUT, PATCH)"""
37
-
38
- url: str
39
- """Fully qualifying URL for this request."""
40
-
41
- endpoint: str
42
- """Endpoint of the URL for this request."""
43
-
44
- data: dict | None
45
- """Relevant data for this request."""
46
-
47
- files: list | None
48
- """Relevant files for this request."""
49
-
50
- future: asyncio.Future
51
- """Track the result of this request."""
52
-
53
- class RouteQueue:
54
- """Represents a queue of requests for a single rate-limit bucket.
55
- Manages task worker that processes requests for that bucket.
56
- """
57
- def __init__(self):
58
- self.queue = asyncio.Queue()
59
- """Queue holding RequestItem for this bucket."""
60
-
61
- self.worker = None
62
- """Process for executing request for this bucket."""
9
+
10
+ class HTTPException(Exception):
11
+ """Represents an HTTP error response from Discord."""
12
+ def __init__(self, response: aiohttp.ClientResponse, message: str):
13
+ self.response = response
14
+ self.status = response.status
15
+ self.text = message
16
+ super().__init__(f"{response.status}: {message}")
63
17
 
64
18
  class HTTPClient:
65
- """Handles all HTTP communication with Discord's API
66
- including rate-limiting by bucket and globally, async request handling,
67
- multipart/form-data file uploads, and error handling/retries.
68
- """
19
+ BASE = "https://discord.com/api/v10"
20
+ MAX_RETRIES = 3
21
+
69
22
  def __init__(self, token: str, logger: Logger):
70
23
  self.token = token
71
- """The bot's token."""
72
-
73
- self._logger = logger
74
- """Logger instance to log events."""
75
-
76
- self.session: aiohttp.ClientSession = None
77
- """Client session instance."""
78
-
79
- self.global_reset = 0
80
- """Global rete limit cooldown if a global rate limit is active."""
81
-
82
- self.global_lock: asyncio.Lock = None
83
- """Lock on queues to avoid race conditions."""
84
-
85
- self.pending_queue: asyncio.Queue = None
86
- """Queue for requests not yet assigned to bucket."""
87
-
88
- self.pending_worker: asyncio.Task = None
89
- """Task processing for the pending queue."""
90
-
91
- self.endpoint_to_bucket: dict[str, str] = {}
92
- """Maps endpoints to rate-limit buckets"""
93
-
94
- self.bucket_queues: dict[str, RouteQueue] = {}
95
- """Maps endpoints to RouteQueue objects."""
96
-
97
- self._sentinel = object()
98
- """Sentinel to terminate session."""
99
-
100
- self.base_url = "https://discord.com/api/v10"
101
- """Base URL for discord's API requests."""
102
-
103
- async def start_session(self):
104
- """Initializes aiohttp session, queues, locks, and starting pending worker."""
105
- self.session = aiohttp.ClientSession()
106
- self.pending_queue = asyncio.Queue()
24
+ self.session: Optional[aiohttp.ClientSession] = None
25
+ self.logger = logger
26
+ self.global_reset = 0.0
107
27
  self.global_lock = asyncio.Lock()
108
- self.pending_worker = asyncio.create_task(self._pending_worker())
109
- self._logger.log_debug("Session started.")
28
+ self.endpoint_to_bucket: dict[str, str] = {}
29
+ self.queues: dict[str, asyncio.Queue] = {}
30
+ self.workers: dict[str, asyncio.Task] = {}
31
+
32
+ async def start(self):
33
+ """Start the HTTP session."""
34
+ if not self.session:
35
+ self.session = aiohttp.ClientSession(
36
+ headers={"Authorization": f"Bot {self.token}"}
37
+ )
38
+
39
+ async def close(self):
40
+ """Close the HTTP session."""
41
+ for task in self.workers.values():
42
+ task.cancel()
43
+ if self.session and not self.session.closed:
44
+ await self.session.close()
110
45
 
111
- async def request(self, method: str, endpoint: str, data=None, params=None, files=None):
46
+ async def request(
47
+ self,
48
+ method: str,
49
+ endpoint: str,
50
+ *,
51
+ data: Any | None = None,
52
+ params: dict | None = None,
53
+ files: Any | None = None,
54
+ ):
112
55
  """Enqueues request WRT rate-limit buckets.
113
56
 
114
57
  Args:
115
58
  method (str): HTTP method (e.g., POST, GET, DELETE, PATCH, etc.)
116
59
  endpoint (str): Discord endpoint (e.g., /channels/123/messages)
117
60
  data (dict, optional): relevant data
61
+ params (dict, optional): relevant query params
118
62
  files (list[str], optional): relevant files
119
63
 
120
64
  Returns:
121
65
  (Future): future with response
122
66
  """
123
- url = f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}" # normalize for single slashes
124
- if params:
125
- url += f"?{urlencode(params)}"
67
+ if not self.session:
68
+ await self.start()
126
69
 
70
+ bucket = self.endpoint_to_bucket.get(endpoint, endpoint)
71
+ queue = self.queues.setdefault(bucket, asyncio.Queue())
127
72
  future = asyncio.get_event_loop().create_future()
128
73
 
129
- if endpoint in self.endpoint_to_bucket:
130
- bucket = self.endpoint_to_bucket[endpoint]
131
- if bucket not in self.bucket_queues:
132
- self.bucket_queues[bucket] = RouteQueue()
133
- self.bucket_queues[bucket].worker = asyncio.create_task(
134
- self._route_worker(bucket)
135
- )
136
- await self.bucket_queues[bucket].queue.put(RequestItem(method, url, endpoint, data, files, future))
137
- else:
138
- await self.pending_queue.put(RequestItem(method, url, endpoint, data, files, future))
74
+ await queue.put((method, endpoint, data, params, files, future))
75
+ if bucket not in self.workers:
76
+ self.workers[bucket] = asyncio.create_task(self._worker(bucket))
139
77
 
140
78
  return await future
141
79
 
142
- async def _pending_worker(self):
143
- """Processes requests from global pending queue."""
144
- while True:
145
- item = await self.pending_queue.get()
146
- if item is self._sentinel:
147
- self.pending_queue.task_done()
148
- break
149
- await self._process_request(item)
150
- self.pending_queue.task_done()
151
-
152
- async def _route_worker(self, bucket: str):
153
- """Processes request from specific rate-limit bucket.
80
+ async def _worker(self, bucket: str):
81
+ """Processes request from specific rate-limit bucket."""
154
82
 
155
- Args:
156
- bucket (str): endpoint
157
- """
158
- queue = self.bucket_queues[bucket].queue
159
- while True:
160
- item = await queue.get()
161
- if item is self._sentinel:
162
- queue.task_done()
163
- break
164
- await self._process_request(item)
165
- queue.task_done()
166
-
167
- async def _process_request(self, item: RequestItem):
168
- """Core request execution. Handles headers, payload, files, retries, and bucket assignment.
83
+ q = self.queues[bucket]
84
+ while self.session:
85
+ method, endpoint, data, params, files, future = await q.get()
86
+ try:
87
+ result = await self._send(method, endpoint, data, params, files)
88
+ if not future.done():
89
+ future.set_result(result)
90
+ except Exception as e:
91
+ if not future.done():
92
+ future.set_exception(e)
93
+ finally:
94
+ q.task_done()
95
+
96
+ async def _send(
97
+ self,
98
+ method: str,
99
+ endpoint: str,
100
+ data: Any | None,
101
+ params: dict | None,
102
+ files: Any | None,
103
+ ):
104
+ """Core HTTP request executor.
105
+
106
+ Sends a request to Discord, handling JSON payloads, files, query parameters,
107
+ rate limits, and retries.
169
108
 
170
109
  Args:
171
- item (RequestItem): incoming request
110
+ method (str): HTTP method (e.g., 'POST', 'GET', 'DELETE', 'PATCH').
111
+ endpoint (str): Discord API endpoint (e.g., '/channels/123/messages').
112
+ data (dict | None, optional): JSON payload to include in the request body.
113
+ params (dict | None, optional): Query parameters to append to the URL.
114
+ files (list[str] | None, optional): Files to send with the request.
172
115
 
173
116
  Raises:
174
- DiscordError: discord error object
117
+ (HTTPException): If the request fails after the maximum number of retries
118
+ or receives an error response.
119
+
120
+ Returns:
121
+ (dict | str | None): Parsed JSON response if available, raw text if the
122
+ response is not JSON, or None for HTTP 204 responses.
175
123
  """
176
- try:
177
- await self._check_global_limit()
178
124
 
179
- headers = {"Authorization": f"Bot {self.token}"}
125
+ url = f"{self.BASE.rstrip('/')}/{endpoint.lstrip('/')}"
180
126
 
181
- # Build multipart if files exist
182
- request_kwargs = {'headers': headers, 'ssl': ssl_ctx}
127
+ def sanitize_query_params(params: dict | None) -> dict | None:
128
+ if not params:
129
+ return None
130
+ return {k: ('true' if v is True else 'false' if v is False else v)
131
+ for k, v in params.items() if v is not None}
183
132
 
184
- if item.files: # only create FormData if files exist
185
- form = aiohttp.FormData()
186
- form.add_field('payload_json', json.dumps(item.data))
133
+ for attempt in range(self.MAX_RETRIES):
134
+ await self._check_global_limit()
187
135
 
188
- for idx, file_path in enumerate(item.files):
189
- try:
190
- async with aiofiles.open(file_path, 'rb') as f:
191
- data = await f.read()
192
- form.add_field(
193
- f'files[{idx}]',
194
- data,
195
- filename=file_path.split('/')[-1],
196
- content_type='application/octet-stream'
197
- )
198
- except FileNotFoundError:
199
- self._logger.log_warn(f"File '{file_path}' could not be found.")
200
- break
201
-
202
- request_kwargs['data'] = form
203
-
204
- elif item.data is not None:
205
- request_kwargs['json'] = item.data # aiohttp sets Content-Type automatically
206
-
207
- async with self.session.request(item.method, item.url, **request_kwargs) as resp:
208
- self._logger.log_debug(f"{item.method} {item.endpoint}: {resp.status} {DISCORD_HTTP_CODES.get(resp.status, 'Unknown Status')}")
209
-
210
- # if triggered rate-limit
211
- if resp.status == 429:
212
- data = await resp.json()
213
- retry_after = float(data.get("retry_after", 1))
214
- is_global = data.get("global")
215
- if is_global:
216
- self.global_reset = time.time() + retry_after
217
-
218
- try:
219
- self._logger.log_warn( f"You are being rate limited on {item.endpoint}. Retrying in {retry_after}s (global={is_global})")
220
- await asyncio.sleep(retry_after + 0.5)
221
- except asyncio.CancelledError:
222
- # shutdown is happening
223
- raise
224
-
225
- # retry the request
226
- await self._process_request(item)
227
- return
228
-
229
- # if response failed (either lethal or recoverable)
230
- elif resp.status not in [200, 201, 204]:
231
- raise DiscordError(resp.status, await resp.json())
232
-
233
- await self._handle_response(item, resp)
234
-
235
- # Handle rate-limit bucket headers
236
- bucket = resp.headers.get("X-RateLimit-Bucket")
237
- if bucket and item.endpoint not in self.endpoint_to_bucket:
238
- self.endpoint_to_bucket[item.endpoint] = bucket
239
- if bucket not in self.bucket_queues:
240
- self.bucket_queues[bucket] = RouteQueue()
241
- self.bucket_queues[bucket].worker = asyncio.create_task(
242
- self._route_worker(bucket)
136
+ kwargs = {}
137
+
138
+ if files and any(files):
139
+ payload, headers = await self._make_payload(data, files)
140
+ kwargs = {"data": payload, "headers": headers}
141
+ else:
142
+ kwargs = {"json": data}
143
+
144
+ try:
145
+ async with self.session.request(
146
+ method, url, params=sanitize_query_params(params), timeout=15, **kwargs
147
+ ) as resp:
148
+ if resp.status == 429:
149
+ data = await resp.json()
150
+ retry = float(data.get("retry_after", 1))
151
+ if data.get("global"):
152
+ self.global_reset = asyncio.get_event_loop().time() + retry
153
+ self.logger.log_warn(
154
+ f"Rate limited {retry}s ({'global' if data.get('global') else 'bucket'})"
243
155
  )
156
+ await asyncio.sleep(retry + 0.5)
157
+ continue
244
158
 
245
- except Exception as e:
246
- if not item.future.done():
247
- item.future.set_exception(e)
159
+ if 200 <= resp.status < 300:
160
+ if resp.status == 204:
161
+ return None
162
+ try:
163
+ return await resp.json()
164
+ except aiohttp.ContentTypeError:
165
+ return await resp.text()
248
166
 
249
- async def _handle_response(self, item: RequestItem, resp: aiohttp.ClientResponse):
250
- """Resolves future with parsed JSON/text response.
167
+ text = await resp.text()
168
+ raise HTTPException(resp, text)
251
169
 
252
- Args:
253
- item (RequestItem): request data to handle
254
- resp (aiohttp.ClientResponse): response for item
255
- """
256
- if resp.status == 204:
257
- item.future.set_result((None, 204))
258
- else:
259
- try:
260
- result = await resp.json()
261
- except aiohttp.ContentTypeError:
262
- result = await resp.text()
263
- item.future.set_result(result)
170
+ except asyncio.TimeoutError:
171
+ self.logger.log_warn(f"Timeout on {method} {endpoint}, retrying...")
172
+ continue
173
+
174
+ raise HTTPException(resp, f"Failed after {self.MAX_RETRIES} retries")
264
175
 
265
176
  async def _check_global_limit(self):
266
177
  """Waits if the global rate-limit is in effect."""
267
- async with self.global_lock:
268
- now = time.time()
269
- if now < self.global_reset:
270
- await asyncio.sleep(self.global_reset - now)
271
-
272
- async def close_session(self):
273
- """Gracefully shuts down all workes and closes aiohttp session."""
274
- # Stop workers
275
- for q in self.bucket_queues.values():
276
- await q.queue.put(self._sentinel)
277
- await self.pending_queue.put(self._sentinel)
278
-
279
- if self.session and not self.session.closed:
280
- await self.session.close()
178
+
179
+ now = asyncio.get_event_loop().time()
180
+ if now < self.global_reset:
181
+ delay = self.global_reset - now
182
+ self.logger.log_warn(f"Global rate limit active, sleeping {delay:.2f}s")
183
+ await asyncio.sleep(delay)
184
+
185
+ async def _make_payload(self, data: dict, files: list):
186
+ """Return (data, headers) for aiohttp request — supports multipart.
187
+
188
+ Args:
189
+ data (dict): request data
190
+ files (list): relevant files
191
+
192
+ Returns:
193
+ (tuple[aiohttp.FormData, dict]): form data and headers
194
+ """
195
+ headers = {}
196
+ if not files:
197
+ return data, headers
198
+
199
+ form = aiohttp.FormData()
200
+ if data:
201
+ form.add_field("payload_json", json.dumps(data))
202
+
203
+ for idx, file_path in enumerate(files):
204
+ async with aiofiles.open(file_path, 'rb') as f:
205
+ data = await f.read()
206
+ form.add_field(
207
+ f'files[{idx}]',
208
+ data,
209
+ filename=file_path.split('/')[-1],
210
+ content_type='application/octet-stream'
211
+ )
212
+
213
+ return form, headers
discord/logger.py CHANGED
@@ -35,6 +35,7 @@ class Logger:
35
35
  """Log file for writing."""
36
36
  except Exception as e:
37
37
  self.log_error(f"Error {type(e)}: {e}")
38
+ self.log_traceback()
38
39
 
39
40
  self.dev_mode = dev_mode
40
41
  """If debug logs should be printed."""
@@ -52,6 +53,11 @@ class Logger:
52
53
 
53
54
  return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
54
55
 
56
+ def log_traceback(self):
57
+ if self.dev_mode == True:
58
+ import traceback
59
+ self._log("DEBUG", self.DEBUG, traceback.format_exc())
60
+
55
61
  def _log(self, level: str, color: str, message: str):
56
62
  """Internal helper that writes formatted log to both file and console.
57
63
 
@@ -148,7 +148,7 @@ class Channel(DataModel):
148
148
  if isinstance(message, str):
149
149
  message = MessageBuilder(content=message)
150
150
 
151
- data = await self._http.request("POST", f"/channels/{self.id}/messages", message._to_dict())
151
+ data = await self._http.request("POST", f"/channels/{self.id}/messages", data=message._to_dict())
152
152
 
153
153
  return Message.from_dict(data, self._http)
154
154
 
@@ -1,5 +1,6 @@
1
1
  from dataclasses import dataclass
2
2
  from typing import Optional, TypedDict, Unpack
3
+ from urllib.parse import urlencode
3
4
 
4
5
  from ..http import HTTPClient
5
6
  from ..model import DataModel
@@ -136,7 +137,7 @@ class Guild(DataModel):
136
137
  Returns:
137
138
  (Channel): the created channel
138
139
  """
139
- data = await self._http.request('POST', f'/guilds/{self.id}/channels', channel._to_dict())
140
+ data = await self._http.request('POST', f'/guilds/{self.id}/channels', data=channel._to_dict())
140
141
 
141
142
  return Channel.from_dict(data, self._http)
142
143
 
@@ -233,7 +234,7 @@ class Guild(DataModel):
233
234
  Returns:
234
235
  (RoleModel): new role data
235
236
  """
236
- data = await self._http.request('POST', f'/guilds/{self.id}/roles', role._to_dict())
237
+ data = await self._http.request('POST', f'/guilds/{self.id}/roles', data=role._to_dict())
237
238
 
238
239
  return RoleModel.from_dict(data)
239
240
 
@@ -249,7 +250,7 @@ class Guild(DataModel):
249
250
  Returns:
250
251
  (RoleModel): role with changes
251
252
  """
252
- data = await self._http.request('PATCH', f'/guilds/{self.id}/roles/{role_id}', role._to_dict())
253
+ data = await self._http.request('PATCH', f'/guilds/{self.id}/roles/{role_id}', data=role._to_dict())
253
254
 
254
255
  return RoleModel.from_dict(data)
255
256
 
@@ -122,7 +122,7 @@ class Interaction(DataModel):
122
122
  data = await self._http.request(
123
123
  'POST',
124
124
  f'/interactions/{self.id}/{self.token}/callback',
125
- content,
125
+ data=content,
126
126
  files=[fp.path for fp in message.attachments],
127
127
  params=params)
128
128
 
@@ -151,7 +151,7 @@ class Interaction(DataModel):
151
151
  await self._http.request(
152
152
  'POST',
153
153
  f'/interactions/{self.id}/{self.token}/callback',
154
- content,
154
+ data=content,
155
155
  files=[fp.path for fp in message.attachments])
156
156
 
157
157
  async def respond_modal(self, modal: ModalBuilder):
@@ -174,4 +174,4 @@ class Interaction(DataModel):
174
174
  await self._http.request(
175
175
  'POST',
176
176
  f'/interactions/{self.id}/{self.token}/callback',
177
- content)
177
+ data=content)
@@ -68,7 +68,7 @@ class Message(DataModel):
68
68
  data = await self._http.request(
69
69
  "POST",
70
70
  f"/channels/{self.channel_id}/messages",
71
- message._to_dict(),
71
+ data=message._to_dict(),
72
72
  files=[fp.path for fp in message.attachments] if message.attachments else None
73
73
  )
74
74
  return Message.from_dict(data, self._http)
@@ -88,7 +88,7 @@ class Message(DataModel):
88
88
  data = await self._http.request(
89
89
  "PATCH",
90
90
  f"/channels/{self.channel_id}/messages/{self.id}",
91
- message._to_dict(),
91
+ data=message._to_dict(),
92
92
  files=[fp.path for fp in message.attachments] if message.attachments else None)
93
93
 
94
94
  self._update(data)
@@ -110,7 +110,7 @@ class Message(DataModel):
110
110
  await self._http.request(
111
111
  'POST',
112
112
  f"/channels/{self.channel_id}/messages",
113
- message._to_dict(),
113
+ data=message._to_dict(),
114
114
  files=[fp.path for fp in message.attachments] if message.attachments else None)
115
115
 
116
116
  async def crosspost(self):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scurrypy
3
- Version: 0.4
3
+ Version: 0.4.1
4
4
  Summary: Dataclass-driven Discord API Wrapper in Python
5
5
  Author: Furmissile
6
6
  Requires-Python: >=3.10
@@ -1,17 +1,17 @@
1
1
  discord/__init__.py,sha256=cETkxHmm0s9YkSJgn-1daQhnbL96fuD7L9SIg2t5vBg,6823
2
- discord/client.py,sha256=9DcgF8Tb-K7t781GILBIh4LcU_Q1aYrNW_mHU-TA0bw,14144
2
+ discord/client.py,sha256=feTk5CXgYEMeV9bd3ahz2TcrjqtjRElVRBGU6wwzaoY,14225
3
3
  discord/client_like.py,sha256=JyJq0XBq0vKuPBJ_ZnYf5yAAuX1zz_2B1TZBQE-BYbQ,473
4
4
  discord/config.py,sha256=OH1A2mNKhDlGvQYASEsVUx2pNxP1YQ2a7a7z-IM5xFg,200
5
5
  discord/error.py,sha256=AlislRTna554cM6KC0KrwKugzYDYtx_9C8_3QFe4XDc,2070
6
- discord/gateway.py,sha256=1TVUKrd3JovoM4df5-GlMZ0kz15Xls5V48ShXDSlK3Q,5334
7
- discord/http.py,sha256=BI7bCjedh8zuu5pdOLhI-XwMZfdVj5qP9ZZY6WJqtgo,10511
6
+ discord/gateway.py,sha256=L0SE7N29rg02JtNv670JMbw8LMLvtEsLF4LPM33OlHM,5110
7
+ discord/http.py,sha256=EfE_4iwGxeCr1MRga-cVvn4rlkBx5C6MkUgq8yGp-Xs,7751
8
8
  discord/intents.py,sha256=Lf2fogFFDqilZeKJv7tcUgKmMW3D7ykK4bBNi-zDzYA,2866
9
- discord/logger.py,sha256=qAmOc3geCcCCqPhdi61SVWzMDewmM8Q_KWhTcjO46j8,4726
9
+ discord/logger.py,sha256=7lks8VyU538nUr_OfiUXRFOtXlpOLZSLvLoDRxJ8loY,4929
10
10
  discord/model.py,sha256=CmuxyoWLWokE_UvCQ9M7U9Cr7JH9R7ULMv9KMwzXjDQ,3105
11
11
  discord/dispatch/__init__.py,sha256=m7ixrbNhOV9QRORXPw6LSwxofQMAvLmPFBweBZu9ACc,20
12
- discord/dispatch/command_dispatcher.py,sha256=pyJOQaZLZYrHUEs6HEWp8XMKTMZX4SBrwTizGKIeUG8,5904
13
- discord/dispatch/event_dispatcher.py,sha256=1A7Qof_IzTi5_14IMPxIQDpvo3-Sj-X0KZWOuVGH53k,3764
14
- discord/dispatch/prefix_dispatcher.py,sha256=4mkn3cuXTjdEChbewkbZQqd_sMKm4jePSFKKOPbt12g,2065
12
+ discord/dispatch/command_dispatcher.py,sha256=4yyw-PspQrKWG40oSWOSz_SmmqG2D2HTpRmaVi19H2Q,5954
13
+ discord/dispatch/event_dispatcher.py,sha256=0hX4oSQloxColXNxPT2iWnqi4S9VKTxyD9_MSPsVYUM,3769
14
+ discord/dispatch/prefix_dispatcher.py,sha256=ikGlYddZawtQ8eVSsbWph-vqh3xc3aheTfOBg7zDeSM,2273
15
15
  discord/events/__init__.py,sha256=xE8YtJ7NKZkm7MLnohDQIbezh3ColmLR-3BMiZabt3k,18
16
16
  discord/events/channel_events.py,sha256=t9UL4JjDqulAP_XepQ8MRMW54pNRqCbIK3M8tauzf9I,1556
17
17
  discord/events/guild_events.py,sha256=Ok9tW3tjcwtbiqJgbe-42d9-R3-2RzqmIgBHEP-2Pcc,896
@@ -42,13 +42,13 @@ discord/parts/role.py,sha256=cK96UdgT-kU0gY5C_1LZXPrYg144x2RDmGjT28so57A,920
42
42
  discord/resources/__init__.py,sha256=EdzYKftSLqqr3Bpzc0_90kfozJXOtp9jNTIHhCTt_-0,21
43
43
  discord/resources/application.py,sha256=vYMTli_FSbC7venMepsJ9bkzdEQVkKYpnxCJ9K2XDho,2765
44
44
  discord/resources/bot_emojis.py,sha256=RvGCSOBkjS39P2aab0FzYUOTzBOiHX99RLrJZzAYNiU,1701
45
- discord/resources/channel.py,sha256=fe2JUp943VnXa-BKyRMtNP-JyNd_Mp516sWBKHKn_GI,6915
46
- discord/resources/guild.py,sha256=Unld1lWY3XynmRHU2FCi3-LA9VNp2thMI2BlILUTTxk,8183
47
- discord/resources/interaction.py,sha256=esbkN8r7c1GhAvRqldrua_a6PSuIVPJLw3OQAZ0zy1c,5922
48
- discord/resources/message.py,sha256=RtvcCRx0lwW-mHPl3aNYoEvGffrvtpLsQ2fVWckywVI,7527
45
+ discord/resources/channel.py,sha256=HC7PCtjP8mjyEZSOhYqoiqJaGbawZA4XOL3GkTlWScA,6920
46
+ discord/resources/guild.py,sha256=jVlkdeMfB97q9qbgOn1g3Wtt0BcL9Ktj4hbbATnnbus,8234
47
+ discord/resources/interaction.py,sha256=sUg5HOZLZvb_LWwHwyfPlcgMMuIXrNpvjVwR2efwVDk,5937
48
+ discord/resources/message.py,sha256=Oo3-EbJMt0UHK77x3CndjYwRrWbp5Vf5UHSIJqXzNHE,7542
49
49
  discord/resources/user.py,sha256=vk89TnCVi-6ZgbDs_TZTCXrx_NfFS5Q9Wi_itYoaoyg,3085
50
- scurrypy-0.4.dist-info/licenses/LICENSE,sha256=qIlBETYpSEU8glbiwiJbuDxVl-2WIuf1PDqJemMjKkc,792
51
- scurrypy-0.4.dist-info/METADATA,sha256=J9enmBVZsPDNbClN0t9p5_SAksFXso23AIjtgBREbNE,4795
52
- scurrypy-0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
- scurrypy-0.4.dist-info/top_level.txt,sha256=fJkrNbR-_8ubMBUcDEJBcfkpECrvSEmMrNKgvLlQFoM,8
54
- scurrypy-0.4.dist-info/RECORD,,
50
+ scurrypy-0.4.1.dist-info/licenses/LICENSE,sha256=qIlBETYpSEU8glbiwiJbuDxVl-2WIuf1PDqJemMjKkc,792
51
+ scurrypy-0.4.1.dist-info/METADATA,sha256=6VFko4N476slDA-Rel1ZPKvghS4NKpUZcvzpb3HSb08,4797
52
+ scurrypy-0.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
+ scurrypy-0.4.1.dist-info/top_level.txt,sha256=fJkrNbR-_8ubMBUcDEJBcfkpECrvSEmMrNKgvLlQFoM,8
54
+ scurrypy-0.4.1.dist-info/RECORD,,