panther 4.2.6__tar.gz → 4.3.1__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 (81) hide show
  1. {panther-4.2.6 → panther-4.3.1}/PKG-INFO +2 -1
  2. {panther-4.2.6 → panther-4.3.1}/panther/__init__.py +1 -1
  3. {panther-4.2.6 → panther-4.3.1}/panther/_load_configs.py +18 -1
  4. {panther-4.2.6 → panther-4.3.1}/panther/_utils.py +9 -9
  5. {panther-4.2.6 → panther-4.3.1}/panther/configs.py +2 -0
  6. {panther-4.2.6 → panther-4.3.1}/panther/main.py +9 -11
  7. {panther-4.2.6 → panther-4.3.1}/panther/response.py +37 -0
  8. {panther-4.2.6 → panther-4.3.1}/panther.egg-info/PKG-INFO +2 -1
  9. {panther-4.2.6 → panther-4.3.1}/panther.egg-info/requires.txt +1 -0
  10. {panther-4.2.6 → panther-4.3.1}/setup.py +1 -0
  11. {panther-4.2.6 → panther-4.3.1}/tests/test_response.py +35 -3
  12. {panther-4.2.6 → panther-4.3.1}/tests/test_utils.py +54 -0
  13. {panther-4.2.6 → panther-4.3.1}/LICENSE +0 -0
  14. {panther-4.2.6 → panther-4.3.1}/README.md +0 -0
  15. {panther-4.2.6 → panther-4.3.1}/panther/app.py +0 -0
  16. {panther-4.2.6 → panther-4.3.1}/panther/authentications.py +0 -0
  17. {panther-4.2.6 → panther-4.3.1}/panther/background_tasks.py +0 -0
  18. {panther-4.2.6 → panther-4.3.1}/panther/base_request.py +0 -0
  19. {panther-4.2.6 → panther-4.3.1}/panther/base_websocket.py +0 -0
  20. {panther-4.2.6 → panther-4.3.1}/panther/caching.py +0 -0
  21. {panther-4.2.6 → panther-4.3.1}/panther/cli/__init__.py +0 -0
  22. {panther-4.2.6 → panther-4.3.1}/panther/cli/create_command.py +0 -0
  23. {panther-4.2.6 → panther-4.3.1}/panther/cli/main.py +0 -0
  24. {panther-4.2.6 → panther-4.3.1}/panther/cli/monitor_command.py +0 -0
  25. {panther-4.2.6 → panther-4.3.1}/panther/cli/run_command.py +0 -0
  26. {panther-4.2.6 → panther-4.3.1}/panther/cli/template.py +0 -0
  27. {panther-4.2.6 → panther-4.3.1}/panther/cli/utils.py +0 -0
  28. {panther-4.2.6 → panther-4.3.1}/panther/db/__init__.py +0 -0
  29. {panther-4.2.6 → panther-4.3.1}/panther/db/connections.py +0 -0
  30. {panther-4.2.6 → panther-4.3.1}/panther/db/cursor.py +0 -0
  31. {panther-4.2.6 → panther-4.3.1}/panther/db/models.py +0 -0
  32. {panther-4.2.6 → panther-4.3.1}/panther/db/queries/__init__.py +0 -0
  33. {panther-4.2.6 → panther-4.3.1}/panther/db/queries/base_queries.py +0 -0
  34. {panther-4.2.6 → panther-4.3.1}/panther/db/queries/mongodb_queries.py +0 -0
  35. {panther-4.2.6 → panther-4.3.1}/panther/db/queries/pantherdb_queries.py +0 -0
  36. {panther-4.2.6 → panther-4.3.1}/panther/db/queries/queries.py +0 -0
  37. {panther-4.2.6 → panther-4.3.1}/panther/db/utils.py +0 -0
  38. {panther-4.2.6 → panther-4.3.1}/panther/events.py +0 -0
  39. {panther-4.2.6 → panther-4.3.1}/panther/exceptions.py +0 -0
  40. {panther-4.2.6 → panther-4.3.1}/panther/file_handler.py +0 -0
  41. {panther-4.2.6 → panther-4.3.1}/panther/generics.py +0 -0
  42. {panther-4.2.6 → panther-4.3.1}/panther/logging.py +0 -0
  43. {panther-4.2.6 → panther-4.3.1}/panther/middlewares/__init__.py +0 -0
  44. {panther-4.2.6 → panther-4.3.1}/panther/middlewares/base.py +0 -0
  45. {panther-4.2.6 → panther-4.3.1}/panther/monitoring.py +0 -0
  46. {panther-4.2.6 → panther-4.3.1}/panther/pagination.py +0 -0
  47. {panther-4.2.6 → panther-4.3.1}/panther/panel/__init__.py +0 -0
  48. {panther-4.2.6 → panther-4.3.1}/panther/panel/apis.py +0 -0
  49. {panther-4.2.6 → panther-4.3.1}/panther/panel/urls.py +0 -0
  50. {panther-4.2.6 → panther-4.3.1}/panther/panel/utils.py +0 -0
  51. {panther-4.2.6 → panther-4.3.1}/panther/permissions.py +0 -0
  52. {panther-4.2.6 → panther-4.3.1}/panther/request.py +0 -0
  53. {panther-4.2.6 → panther-4.3.1}/panther/routings.py +0 -0
  54. {panther-4.2.6 → panther-4.3.1}/panther/serializer.py +0 -0
  55. {panther-4.2.6 → panther-4.3.1}/panther/status.py +0 -0
  56. {panther-4.2.6 → panther-4.3.1}/panther/test.py +0 -0
  57. {panther-4.2.6 → panther-4.3.1}/panther/throttling.py +0 -0
  58. {panther-4.2.6 → panther-4.3.1}/panther/utils.py +0 -0
  59. {panther-4.2.6 → panther-4.3.1}/panther/websocket.py +0 -0
  60. {panther-4.2.6 → panther-4.3.1}/panther.egg-info/SOURCES.txt +0 -0
  61. {panther-4.2.6 → panther-4.3.1}/panther.egg-info/dependency_links.txt +0 -0
  62. {panther-4.2.6 → panther-4.3.1}/panther.egg-info/entry_points.txt +0 -0
  63. {panther-4.2.6 → panther-4.3.1}/panther.egg-info/top_level.txt +0 -0
  64. {panther-4.2.6 → panther-4.3.1}/pyproject.toml +0 -0
  65. {panther-4.2.6 → panther-4.3.1}/setup.cfg +0 -0
  66. {panther-4.2.6 → panther-4.3.1}/tests/test_authentication.py +0 -0
  67. {panther-4.2.6 → panther-4.3.1}/tests/test_background_tasks.py +0 -0
  68. {panther-4.2.6 → panther-4.3.1}/tests/test_caching.py +0 -0
  69. {panther-4.2.6 → panther-4.3.1}/tests/test_cli.py +0 -0
  70. {panther-4.2.6 → panther-4.3.1}/tests/test_database.py +0 -0
  71. {panther-4.2.6 → panther-4.3.1}/tests/test_events.py +0 -0
  72. {panther-4.2.6 → panther-4.3.1}/tests/test_generics.py +0 -0
  73. {panther-4.2.6 → panther-4.3.1}/tests/test_multipart.py +0 -0
  74. {panther-4.2.6 → panther-4.3.1}/tests/test_panel_apis.py +0 -0
  75. {panther-4.2.6 → panther-4.3.1}/tests/test_request.py +0 -0
  76. {panther-4.2.6 → panther-4.3.1}/tests/test_routing.py +0 -0
  77. {panther-4.2.6 → panther-4.3.1}/tests/test_run.py +0 -0
  78. {panther-4.2.6 → panther-4.3.1}/tests/test_serializer.py +0 -0
  79. {panther-4.2.6 → panther-4.3.1}/tests/test_status.py +0 -0
  80. {panther-4.2.6 → panther-4.3.1}/tests/test_throttling.py +0 -0
  81. {panther-4.2.6 → panther-4.3.1}/tests/test_websockets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: panther
3
- Version: 4.2.6
3
+ Version: 4.3.1
4
4
  Summary: Fast & Friendly, Web Framework For Building Async APIs
5
5
  Home-page: https://github.com/alirn76/panther
6
6
  Author: Ali RajabNezhad
@@ -19,6 +19,7 @@ Requires-Dist: pydantic~=2.7.4
19
19
  Requires-Dist: rich~=13.7.1
20
20
  Requires-Dist: uvicorn~=0.27.1
21
21
  Requires-Dist: pytz~=2024.1
22
+ Requires-Dist: Jinja2~=3.1
22
23
  Provides-Extra: full
23
24
  Requires-Dist: redis==5.0.1; extra == "full"
24
25
  Requires-Dist: motor~=3.5.0; extra == "full"
@@ -1,6 +1,6 @@
1
1
  from panther.main import Panther # noqa: F401
2
2
 
3
- __version__ = '4.2.6'
3
+ __version__ = '4.3.1'
4
4
 
5
5
 
6
6
  def version():
@@ -1,9 +1,10 @@
1
1
  import logging
2
2
  import sys
3
+ import types
3
4
  from importlib import import_module
4
5
  from multiprocessing import Manager
5
6
 
6
- from panther._utils import import_class
7
+ from panther._utils import import_class, check_function_type_endpoint, check_class_type_endpoint
7
8
  from panther.background_tasks import background_tasks
8
9
  from panther.base_websocket import WebsocketConnections
9
10
  from panther.cli.utils import import_error
@@ -29,12 +30,14 @@ __all__ = (
29
30
  'load_user_model',
30
31
  'load_log_queries',
31
32
  'load_middlewares',
33
+ 'load_templates_dir',
32
34
  'load_auto_reformat',
33
35
  'load_background_tasks',
34
36
  'load_default_cache_exp',
35
37
  'load_authentication_class',
36
38
  'load_urls',
37
39
  'load_websocket_connections',
40
+ 'check_endpoints_inheritance',
38
41
  )
39
42
 
40
43
  logger = logging.getLogger('panther')
@@ -82,6 +85,11 @@ def load_timezone(_configs: dict, /) -> None:
82
85
  config.TIMEZONE = timezone
83
86
 
84
87
 
88
+ def load_templates_dir(_configs: dict, /) -> None:
89
+ if templates_dir := _configs.get('TEMPLATES_DIR'):
90
+ config.TEMPLATES_DIR = templates_dir
91
+
92
+
85
93
  def load_database(_configs: dict, /) -> None:
86
94
  database_config = _configs.get('DATABASE', {})
87
95
  if 'engine' in database_config:
@@ -259,5 +267,14 @@ def load_websocket_connections():
259
267
  config.WEBSOCKET_CONNECTIONS = WebsocketConnections(pubsub_connection=pubsub_connection)
260
268
 
261
269
 
270
+ def check_endpoints_inheritance():
271
+ """Should be after `load_urls()`"""
272
+ for _, endpoint in config.FLAT_URLS.items():
273
+ if isinstance(endpoint, types.FunctionType):
274
+ check_function_type_endpoint(endpoint=endpoint)
275
+ else:
276
+ check_class_type_endpoint(endpoint=endpoint)
277
+
278
+
262
279
  def _exception_handler(field: str, error: str | Exception) -> PantherError:
263
280
  return PantherError(f"Invalid '{field}': {error}")
@@ -4,12 +4,13 @@ import logging
4
4
  import re
5
5
  import subprocess
6
6
  import types
7
- from typing import Any, Generator, Iterator, AsyncGenerator
8
7
  from collections.abc import Callable
9
8
  from traceback import TracebackException
9
+ from typing import Any, Generator, Iterator, AsyncGenerator
10
10
 
11
11
  from panther.exceptions import PantherError
12
12
  from panther.file_handler import File
13
+ from panther.websocket import GenericWebsocket
13
14
 
14
15
  logger = logging.getLogger('panther')
15
16
 
@@ -99,19 +100,18 @@ def reformat_code(base_dir):
99
100
  def check_function_type_endpoint(endpoint: types.FunctionType) -> Callable:
100
101
  # Function Doesn't Have @API Decorator
101
102
  if not hasattr(endpoint, '__wrapped__'):
102
- logger.critical(f'You may have forgotten to use @API() on the {endpoint.__name__}()')
103
- raise TypeError
104
- return endpoint
103
+ raise PantherError(
104
+ f'You may have forgotten to use `@API()` on the `{endpoint.__module__}.{endpoint.__name__}()`')
105
105
 
106
106
 
107
107
  def check_class_type_endpoint(endpoint: Callable) -> Callable:
108
108
  from panther.app import GenericAPI
109
109
 
110
- if not issubclass(endpoint, GenericAPI):
111
- logger.critical(f'You may have forgotten to inherit from GenericAPI on the {endpoint.__name__}()')
112
- raise TypeError
113
-
114
- return endpoint().call_method
110
+ if not issubclass(endpoint, (GenericAPI, GenericWebsocket)):
111
+ raise PantherError(
112
+ f'You may have forgotten to inherit from `panther.app.GenericAPI` or `panther.app.GenericWebsocket` '
113
+ f'on the `{endpoint.__module__}.{endpoint.__name__}()`'
114
+ )
115
115
 
116
116
 
117
117
  def async_next(iterator: Iterator):
@@ -67,6 +67,7 @@ class Config:
67
67
  STARTUPS: list[Callable]
68
68
  SHUTDOWNS: list[Callable]
69
69
  TIMEZONE: str
70
+ TEMPLATES_DIR: str | list[str]
70
71
  AUTO_REFORMAT: bool
71
72
  QUERY_ENGINE: typing.Callable | None
72
73
  DATABASE: typing.Callable | None
@@ -110,6 +111,7 @@ default_configs = {
110
111
  'STARTUPS': [],
111
112
  'SHUTDOWNS': [],
112
113
  'TIMEZONE': 'UTC',
114
+ 'TEMPLATES_DIR': 'templates',
113
115
  'AUTO_REFORMAT': False,
114
116
  'QUERY_ENGINE': None,
115
117
  'DATABASE': None,
@@ -58,6 +58,7 @@ class Panther:
58
58
  load_throttling(self._configs_module)
59
59
  load_user_model(self._configs_module)
60
60
  load_log_queries(self._configs_module)
61
+ load_templates_dir(self._configs_module)
61
62
  load_middlewares(self._configs_module)
62
63
  load_auto_reformat(self._configs_module)
63
64
  load_background_tasks(self._configs_module)
@@ -66,6 +67,8 @@ class Panther:
66
67
  load_urls(self._configs_module, urls=self._urls)
67
68
  load_websocket_connections()
68
69
 
70
+ check_endpoints_inheritance()
71
+
69
72
  async def __call__(self, scope: dict, receive: Callable, send: Callable) -> None:
70
73
  if scope['type'] == 'lifespan':
71
74
  message = await receive()
@@ -159,19 +162,10 @@ class Panther:
159
162
  await request.read_body()
160
163
 
161
164
  # Find Endpoint
162
- _endpoint, found_path = find_endpoint(path=request.path)
163
- if _endpoint is None:
165
+ endpoint, found_path = find_endpoint(path=request.path)
166
+ if endpoint is None:
164
167
  return await self._raise(send, monitoring=monitoring, status_code=status.HTTP_404_NOT_FOUND)
165
168
 
166
- # Check Endpoint Type
167
- try:
168
- if isinstance(_endpoint, types.FunctionType):
169
- endpoint = check_function_type_endpoint(endpoint=_endpoint)
170
- else:
171
- endpoint = check_class_type_endpoint(endpoint=_endpoint)
172
- except TypeError:
173
- return await self._raise(send, monitoring=monitoring, status_code=status.HTTP_501_NOT_IMPLEMENTED)
174
-
175
169
  # Collect Path Variables
176
170
  request.collect_path_variables(found_path=found_path)
177
171
 
@@ -185,6 +179,10 @@ class Panther:
185
179
  f'Make sure to return the `request` at the end of `{middleware.__class__.__name__}.before()`')
186
180
  return await self._raise(send, monitoring=monitoring)
187
181
 
182
+ # Prepare the method
183
+ if not isinstance(endpoint, types.FunctionType):
184
+ endpoint = endpoint().call_method
185
+
188
186
  # Call Endpoint
189
187
  response = await endpoint(request=request)
190
188
 
@@ -1,11 +1,22 @@
1
1
  import asyncio
2
+ from sys import version_info
2
3
  from types import NoneType
3
4
  from typing import Generator, AsyncGenerator, Any, Type
4
5
 
6
+ if version_info >= (3, 11):
7
+ from typing import LiteralString
8
+ else:
9
+ from typing import TypeVar
10
+
11
+ LiteralString = TypeVar('LiteralString')
12
+
13
+
5
14
  import orjson as json
6
15
  from pydantic import BaseModel
16
+ from jinja2 import Environment, FileSystemLoader
7
17
 
8
18
  from panther import status
19
+ from panther.configs import config
9
20
  from panther._utils import to_async_generator
10
21
  from panther.db.cursor import Cursor
11
22
  from pantherdb import Cursor as PantherDBCursor
@@ -215,3 +226,29 @@ class PlainTextResponse(Response):
215
226
  if isinstance(self.data, bytes):
216
227
  return self.data
217
228
  return self.data.encode()
229
+
230
+
231
+ class TemplateResponse(HTMLResponse):
232
+ environment = Environment(loader=FileSystemLoader(config.TEMPLATES_DIR))
233
+
234
+ def __init__(
235
+ self,
236
+ source: str | LiteralString | NoneType = None,
237
+ path: str | NoneType = None,
238
+ context: dict | NoneType = None,
239
+ headers: dict | NoneType = None,
240
+ status_code: int = status.HTTP_200_OK,
241
+ pagination: Pagination | NoneType = None,
242
+ ):
243
+ """
244
+ :param source: should be a string
245
+ :param path: should be path of template file
246
+ :param context: should be dict of items
247
+ :param headers: should be dict of headers
248
+ :param status_code: should be int
249
+ :param pagination: instance of Pagination or None
250
+ Its template() method will be used
251
+ """
252
+
253
+ template = self.environment.get_template(path) if path is not None else self.environment.from_string(source)
254
+ super().__init__(template.render(context), headers, status_code, pagination=pagination)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: panther
3
- Version: 4.2.6
3
+ Version: 4.3.1
4
4
  Summary: Fast & Friendly, Web Framework For Building Async APIs
5
5
  Home-page: https://github.com/alirn76/panther
6
6
  Author: Ali RajabNezhad
@@ -19,6 +19,7 @@ Requires-Dist: pydantic~=2.7.4
19
19
  Requires-Dist: rich~=13.7.1
20
20
  Requires-Dist: uvicorn~=0.27.1
21
21
  Requires-Dist: pytz~=2024.1
22
+ Requires-Dist: Jinja2~=3.1
22
23
  Provides-Extra: full
23
24
  Requires-Dist: redis==5.0.1; extra == "full"
24
25
  Requires-Dist: motor~=3.5.0; extra == "full"
@@ -4,6 +4,7 @@ pydantic~=2.7.4
4
4
  rich~=13.7.1
5
5
  uvicorn~=0.27.1
6
6
  pytz~=2024.1
7
+ Jinja2~=3.1
7
8
 
8
9
  [full]
9
10
  redis==5.0.1
@@ -56,6 +56,7 @@ setup(
56
56
  'rich~=13.7.1',
57
57
  'uvicorn~=0.27.1',
58
58
  'pytz~=2024.1',
59
+ 'Jinja2~=3.1',
59
60
  ],
60
61
  extras_require=EXTRAS_REQUIRE,
61
62
  )
@@ -2,7 +2,7 @@ from unittest import IsolatedAsyncioTestCase
2
2
 
3
3
  from panther import Panther
4
4
  from panther.app import API, GenericAPI
5
- from panther.response import Response, HTMLResponse, PlainTextResponse, StreamingResponse
5
+ from panther.response import Response, HTMLResponse, PlainTextResponse, StreamingResponse, TemplateResponse
6
6
  from panther.test import APIClient
7
7
 
8
8
 
@@ -116,6 +116,18 @@ class ReturnHTMLResponse(GenericAPI):
116
116
  return HTMLResponse('<html><head><title></title></head></html>')
117
117
 
118
118
 
119
+ @API()
120
+ async def return_template_response() -> TemplateResponse:
121
+ return TemplateResponse(source='<html><body><p>{{ content }}</p></body></html>', context={'content': 'Hello World'})
122
+
123
+
124
+ class ReturnTemplateResponse(GenericAPI):
125
+ def get(self) -> TemplateResponse:
126
+ return TemplateResponse(
127
+ source='<html><body><p>{{ content }}</p></body></html>', context={'content': 'Hello World'}
128
+ )
129
+
130
+
119
131
  @API()
120
132
  async def return_plain_response():
121
133
  return PlainTextResponse('Hello World')
@@ -160,7 +172,7 @@ urls = {
160
172
  'response-tuple': return_response_tuple,
161
173
  'html': return_html_response,
162
174
  'plain': return_plain_response,
163
-
175
+ 'template': return_template_response,
164
176
  'nothing-cls': ReturnNothing,
165
177
  'none-cls': ReturnNone,
166
178
  'dict-cls': ReturnDict,
@@ -172,8 +184,8 @@ urls = {
172
184
  'response-list-cls': ReturnResponseList,
173
185
  'response-tuple-cls': ReturnResponseTuple,
174
186
  'html-cls': ReturnHTMLResponse,
187
+ 'template-cls': ReturnTemplateResponse,
175
188
  'plain-cls': ReturnPlainResponse,
176
-
177
189
  'stream': ReturnStreamingResponse,
178
190
  'async-stream': ReturnAsyncStreamingResponse,
179
191
  'invalid-status-code': ReturnInvalidStatusCode,
@@ -406,6 +418,26 @@ class TestResponses(IsolatedAsyncioTestCase):
406
418
  assert res.headers['Access-Control-Allow-Origin'] == '*'
407
419
  assert res.headers['Content-Length'] == '41'
408
420
 
421
+ async def test_response_template(self) -> None:
422
+ res: Response = await self.client.get('template/')
423
+ assert res.status_code == 200
424
+ assert res.data == '<html><body><p>Hello World</p></body></html>'
425
+ assert res.body == b'<html><body><p>Hello World</p></body></html>'
426
+ assert set(res.headers.keys()) == {'Content-Type', 'Access-Control-Allow-Origin', 'Content-Length'}
427
+ assert res.headers['Content-Type'] == 'text/html; charset=utf-8'
428
+ assert res.headers['Access-Control-Allow-Origin'] == '*'
429
+ assert res.headers['Content-Length'] == '44'
430
+
431
+ async def test_response_template_cls(self) -> None:
432
+ res: Response = await self.client.get('template-cls/')
433
+ assert res.status_code == 200
434
+ assert res.data == '<html><body><p>Hello World</p></body></html>'
435
+ assert res.body == b'<html><body><p>Hello World</p></body></html>'
436
+ assert set(res.headers.keys()) == {'Content-Type', 'Access-Control-Allow-Origin', 'Content-Length'}
437
+ assert res.headers['Content-Type'] == 'text/html; charset=utf-8'
438
+ assert res.headers['Access-Control-Allow-Origin'] == '*'
439
+ assert res.headers['Content-Length'] == '44'
440
+
409
441
  async def test_response_plain(self):
410
442
  res = await self.client.get('plain/')
411
443
  assert res.status_code == 200
@@ -379,6 +379,60 @@ class TestLoadConfigs(TestCase):
379
379
  AUTHENTICATION = None
380
380
  SECRET_KEY = None
381
381
 
382
+ def test_check_function_endpoint_decorator(self):
383
+ with self.assertLogs(level='ERROR') as captured:
384
+ try:
385
+ Panther(name=__name__, configs=__name__, urls={'/': invalid_api})
386
+ except SystemExit:
387
+ assert True
388
+ else:
389
+ assert False
390
+
391
+ assert len(captured.records) == 1
392
+ assert captured.records[0].getMessage() == 'You may have forgotten to use `@API()` on the `tests.test_utils.invalid_api()`'
393
+
394
+ def test_check_class_endpoint_inheritance(self):
395
+ with self.assertLogs(level='ERROR') as captured:
396
+ try:
397
+ Panther(name=__name__, configs=__name__, urls={'/': InvalidAPI})
398
+ except SystemExit:
399
+ assert True
400
+ else:
401
+ assert False
402
+
403
+ assert len(captured.records) == 1
404
+ assert captured.records[0].getMessage() == (
405
+ f'You may have forgotten to inherit from `panther.app.GenericAPI` or `panther.app.GenericWebsocket` '
406
+ f'on the `tests.test_utils.InvalidAPI()`'
407
+ )
408
+
409
+ def test_check_websocket_inheritance(self):
410
+ with self.assertLogs(level='ERROR') as captured:
411
+ try:
412
+ Panther(name=__name__, configs=__name__, urls={'/': InvalidWebsocket})
413
+ except SystemExit:
414
+ assert True
415
+ else:
416
+ assert False
417
+
418
+ assert len(captured.records) == 1
419
+ assert captured.records[0].getMessage() == (
420
+ f'You may have forgotten to inherit from `panther.app.GenericAPI` or `panther.app.GenericWebsocket` '
421
+ f'on the `tests.test_utils.InvalidWebsocket()`'
422
+ )
423
+
424
+
425
+ def invalid_api():
426
+ pass
427
+
428
+
429
+ class InvalidAPI:
430
+ pass
431
+
432
+
433
+ class InvalidWebsocket:
434
+ pass
435
+
382
436
 
383
437
  class CorrectTestMiddleware(BaseMiddleware):
384
438
  pass
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes