fastapi-interactions 0.0.1__tar.gz → 0.1.0__tar.gz

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.
Files changed (16) hide show
  1. {fastapi_interactions-0.0.1 → fastapi_interactions-0.1.0}/PKG-INFO +1 -1
  2. {fastapi_interactions-0.0.1 → fastapi_interactions-0.1.0}/fastapi_interactions/bot.py +24 -14
  3. {fastapi_interactions-0.0.1 → fastapi_interactions-0.1.0}/fastapi_interactions/commands.py +60 -20
  4. {fastapi_interactions-0.0.1 → fastapi_interactions-0.1.0}/fastapi_interactions/models.py +22 -2
  5. {fastapi_interactions-0.0.1 → fastapi_interactions-0.1.0}/fastapi_interactions.egg-info/PKG-INFO +1 -1
  6. {fastapi_interactions-0.0.1 → fastapi_interactions-0.1.0}/pyproject.toml +1 -1
  7. {fastapi_interactions-0.0.1 → fastapi_interactions-0.1.0}/README.md +0 -0
  8. {fastapi_interactions-0.0.1 → fastapi_interactions-0.1.0}/fastapi_interactions/__init__.py +0 -0
  9. {fastapi_interactions-0.0.1 → fastapi_interactions-0.1.0}/fastapi_interactions/context.py +0 -0
  10. {fastapi_interactions-0.0.1 → fastapi_interactions-0.1.0}/fastapi_interactions/middleware.py +0 -0
  11. {fastapi_interactions-0.0.1 → fastapi_interactions-0.1.0}/fastapi_interactions/responses.py +0 -0
  12. {fastapi_interactions-0.0.1 → fastapi_interactions-0.1.0}/fastapi_interactions.egg-info/SOURCES.txt +0 -0
  13. {fastapi_interactions-0.0.1 → fastapi_interactions-0.1.0}/fastapi_interactions.egg-info/dependency_links.txt +0 -0
  14. {fastapi_interactions-0.0.1 → fastapi_interactions-0.1.0}/fastapi_interactions.egg-info/requires.txt +0 -0
  15. {fastapi_interactions-0.0.1 → fastapi_interactions-0.1.0}/fastapi_interactions.egg-info/top_level.txt +0 -0
  16. {fastapi_interactions-0.0.1 → fastapi_interactions-0.1.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-interactions
3
- Version: 0.0.1
3
+ Version: 0.1.0
4
4
  Summary: A lightweight framework for discord interactions over http built on FastAPI
5
5
  Author-email: Haider Ali <haideralidevnull@gmail.com>
6
6
  Requires-Python: >=3.13
@@ -1,24 +1,26 @@
1
+ import importlib
2
+ import inspect
3
+ import json
4
+ import pkgutil
5
+ import types
6
+ from typing import Any
7
+
8
+ import httpx
1
9
  from fastapi import FastAPI, Request
2
- from .middleware import VerifySignatureMiddleware
3
- from .responses import InteractionResponse, PongResponse, MessageResponse
10
+ from fastapi.responses import JSONResponse
11
+ from loguru import logger
12
+ from pydantic import ValidationError
13
+
14
+ from .commands import Command, CommandRouter
4
15
  from .context import Context
16
+ from .middleware import VerifySignatureMiddleware
5
17
  from .models import (
6
- InteractionType,
7
18
  ApplicationCommandData,
8
19
  Interaction,
20
+ InteractionType,
9
21
  Snowflake,
10
22
  )
11
- from typing import Any
12
- from .commands import Command
13
- from pydantic import ValidationError
14
- from .commands import CommandRouter
15
- import json
16
- import httpx
17
- import importlib
18
- import pkgutil
19
- from loguru import logger
20
- import inspect
21
- import types
23
+ from .responses import InteractionResponse, MessageResponse, PongResponse
22
24
 
23
25
 
24
26
  async def call_with_options(callback: callable, ctx: Context) -> Any:
@@ -107,6 +109,7 @@ class Bot:
107
109
  # print(e.errors())
108
110
  return await MessageResponse("Unexpected error", ephemeral=True)
109
111
 
112
+ raise Exception("Unexpected error occurred")
110
113
  context = Context(
111
114
  interaction=interaction, options=application_command, http=self.http
112
115
  )
@@ -117,6 +120,12 @@ class Bot:
117
120
 
118
121
  return await MessageResponse("Unsupported interaction received", ephemeral=True)
119
122
 
123
+ async def add_error_handler(self, exc: Exception, func: callable):
124
+ pass
125
+
126
+ async def _handle_exception(self, request: Request, exc: Exception):
127
+ return JSONResponse(await MessageResponse(str(exc), ephemeral=True))
128
+
120
129
  def _register_routes(self) -> None:
121
130
  """Set up middleware and the interactions endpoint.
122
131
 
@@ -124,6 +133,7 @@ class Bot:
124
133
  for the interactions endpoint. Called during initialization.
125
134
  """
126
135
  self.app.add_middleware(VerifySignatureMiddleware, public_key=self.public_key)
136
+ self.app.add_exception_handler(Exception, self._handle_exception)
127
137
  self.app.add_api_route(
128
138
  self.interactions_path, endpoint=self.process_interactions, methods=["POST"]
129
139
  )
@@ -1,7 +1,12 @@
1
- from dataclasses import dataclass, field
2
- from .models import Snowflake, ApplicationCommandOptionType
3
- from typing import Optional
4
1
  import sys
2
+ from dataclasses import dataclass, field
3
+
4
+ from .models import (
5
+ ApplicationCommandOptionType,
6
+ IntegrationType,
7
+ InteractionContext,
8
+ Snowflake,
9
+ )
5
10
 
6
11
  OptionType = ApplicationCommandOptionType
7
12
 
@@ -28,27 +33,57 @@ class CommandMeta:
28
33
  description: str
29
34
  options: list[CommandOption] = field(default_factory=list)
30
35
  type: int = 1
36
+ integration_types: IntegrationType | None = None
37
+ contexts: InteractionContext | None = None
31
38
 
32
39
  def as_payload(self):
33
- return {
40
+ payload = {
34
41
  "name": self.name,
35
42
  "description": self.description,
36
43
  "type": self.type,
37
44
  "options": [option.as_payload() for option in self.options],
38
45
  }
46
+ if self.integration_types is not None:
47
+ payload["integration_types"] = self.integration_types.to_api()
48
+ if self.contexts is not None:
49
+ payload["contexts"] = self.contexts.to_api()
50
+ return payload
39
51
 
40
52
 
41
53
  @dataclass(slots=True)
42
54
  class Command:
43
55
  callback: callable
44
56
  meta: CommandMeta
45
- guild_id: Optional[Snowflake] = None
57
+ guild_id: Snowflake | None = None
46
58
 
47
59
 
48
60
  class CommandRouter:
49
- def __init__(self, name: str = None, guild_id: Optional[Snowflake] = None):
61
+ def __init__(
62
+ self,
63
+ name: str | None = None,
64
+ guild_id: Snowflake | None = None,
65
+ *,
66
+ integration_types: IntegrationType | None = None,
67
+ contexts: InteractionContext | None = None,
68
+ ):
50
69
  self.name = name or self.__infer_name()
51
- self.guild_id: Optional[Snowflake] = guild_id
70
+ self.guild_id = guild_id
71
+
72
+ if guild_id is not None and (
73
+ integration_types is not None or contexts is not None
74
+ ):
75
+ raise ValueError(
76
+ f"Router {self.name} is guild scoped. Cannt bind integration/interaction types"
77
+ )
78
+
79
+ if integration_types is not None and not integration_types:
80
+ raise ValueError("integration types cannot be empty;")
81
+
82
+ if contexts is not None and not contexts:
83
+ raise ValueError("contexts cannot be empty")
84
+
85
+ self.integration_types = integration_types
86
+ self.contexts = contexts
52
87
  self.commands: dict[str, Command] = {}
53
88
 
54
89
  def __infer_name(self) -> str:
@@ -86,6 +121,8 @@ class CommandRouter:
86
121
  )
87
122
  meta.name = name
88
123
  meta.description = description
124
+ meta.integration_types = self.integration_types
125
+ meta.contexts = self.contexts
89
126
  self.commands[name] = Command(
90
127
  callback=func, meta=meta, guild_id=self.guild_id
91
128
  )
@@ -97,10 +134,12 @@ class CommandRouter:
97
134
 
98
135
  class Option:
99
136
  @staticmethod
100
- def __create_operation_decorator(name: str, description: str, option_type: OptionType, required: bool = True):
137
+ def __create_operation_decorator(
138
+ name: str, description: str, option_type: OptionType, required: bool = True
139
+ ):
101
140
  def decorator(func):
102
141
  meta = getattr(
103
- func, '_command_meta', CommandMeta(name='', description='', type=1)
142
+ func, "_command_meta", CommandMeta(name="", description="", type=1)
104
143
  )
105
144
 
106
145
  meta.options.insert(
@@ -109,11 +148,12 @@ class Option:
109
148
  name=name,
110
149
  description=description,
111
150
  type=option_type,
112
- required=required
113
- )
151
+ required=required,
152
+ ),
114
153
  )
115
154
  func._command_meta = meta
116
155
  return func
156
+
117
157
  return decorator
118
158
 
119
159
  @staticmethod
@@ -143,7 +183,7 @@ class Option:
143
183
  name=name,
144
184
  description=description,
145
185
  option_type=OptionType.STRING,
146
- required=required
186
+ required=required,
147
187
  )
148
188
 
149
189
  @staticmethod
@@ -173,7 +213,7 @@ class Option:
173
213
  name=name,
174
214
  description=description,
175
215
  option_type=OptionType.INTEGER,
176
- required=required
216
+ required=required,
177
217
  )
178
218
 
179
219
  @staticmethod
@@ -204,7 +244,7 @@ class Option:
204
244
  name=name,
205
245
  description=description,
206
246
  option_type=OptionType.USER,
207
- required=required
247
+ required=required,
208
248
  )
209
249
 
210
250
  @staticmethod
@@ -235,7 +275,7 @@ class Option:
235
275
  name=name,
236
276
  description=description,
237
277
  option_type=OptionType.CHANNEL,
238
- required=required
278
+ required=required,
239
279
  )
240
280
 
241
281
  @staticmethod
@@ -266,7 +306,7 @@ class Option:
266
306
  name=name,
267
307
  description=description,
268
308
  option_type=OptionType.ROLE,
269
- required=required
309
+ required=required,
270
310
  )
271
311
 
272
312
  @staticmethod
@@ -296,7 +336,7 @@ class Option:
296
336
  name=name,
297
337
  description=description,
298
338
  option_type=OptionType.MENTIONABLE,
299
- required=required
339
+ required=required,
300
340
  )
301
341
 
302
342
  @staticmethod
@@ -331,7 +371,7 @@ class Option:
331
371
  name=name,
332
372
  description=description,
333
373
  option_type=OptionType.BOOLEAN,
334
- required=required
374
+ required=required,
335
375
  )
336
376
 
337
377
  @staticmethod
@@ -362,5 +402,5 @@ class Option:
362
402
  name=name,
363
403
  description=description,
364
404
  option_type=OptionType.NUMBER,
365
- required=required
366
- )
405
+ required=required,
406
+ )
@@ -1,10 +1,30 @@
1
- from pydantic import BaseModel, ConfigDict, Field
2
- from typing import Optional, Any
3
1
  from enum import IntEnum, IntFlag
2
+ from typing import Any, Optional
3
+
4
+ from pydantic import BaseModel, ConfigDict, Field
4
5
 
5
6
  Snowflake = str
6
7
 
7
8
 
9
+ class _ApiFlag(IntFlag):
10
+ def to_api(self) -> list[int]:
11
+ """Bit positions set in this flag, e.g. GUILD|PRIVATE_CHANNEL -> [0, 2]."""
12
+ return [i for i in range(self.bit_length()) if self & (1 << i)]
13
+
14
+
15
+ class IntegrationType(_ApiFlag):
16
+ GUILD_INSTALL = 1 << 0 # API VALUE 0
17
+ USER_INSTALL = 1 << 1 # API VALUE 1
18
+ ALL = GUILD_INSTALL | USER_INSTALL
19
+
20
+
21
+ class InteractionContext(_ApiFlag):
22
+ GUILD = 1 << 0 # API VALUE 0
23
+ BOT_DM = 1 << 1 # API VALUE 1
24
+ PRIVATE_CHANNEL = 1 << 2 # VALUE VALUE 2
25
+ ALL = GUILD | BOT_DM | PRIVATE_CHANNEL
26
+
27
+
8
28
  class InteractionType(IntEnum):
9
29
  PING = 1
10
30
  APPLICATION_COMMAND = 2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-interactions
3
- Version: 0.0.1
3
+ Version: 0.1.0
4
4
  Summary: A lightweight framework for discord interactions over http built on FastAPI
5
5
  Author-email: Haider Ali <haideralidevnull@gmail.com>
6
6
  Requires-Python: >=3.13
@@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta'
4
4
 
5
5
  [project]
6
6
  name = 'fastapi-interactions'
7
- version = '0.0.1'
7
+ version = '0.1.0'
8
8
  description='A lightweight framework for discord interactions over http built on FastAPI'
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"