panther 4.3.7__py3-none-any.whl → 5.0.0b1__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.
Files changed (59) hide show
  1. panther/__init__.py +1 -1
  2. panther/_load_configs.py +78 -64
  3. panther/_utils.py +1 -1
  4. panther/app.py +126 -60
  5. panther/authentications.py +26 -9
  6. panther/base_request.py +27 -2
  7. panther/base_websocket.py +26 -27
  8. panther/cli/create_command.py +1 -0
  9. panther/cli/main.py +19 -27
  10. panther/cli/monitor_command.py +8 -4
  11. panther/cli/template.py +11 -6
  12. panther/cli/utils.py +3 -2
  13. panther/configs.py +7 -9
  14. panther/db/cursor.py +23 -7
  15. panther/db/models.py +26 -19
  16. panther/db/queries/base_queries.py +1 -1
  17. panther/db/queries/mongodb_queries.py +172 -10
  18. panther/db/queries/pantherdb_queries.py +5 -5
  19. panther/db/queries/queries.py +1 -1
  20. panther/events.py +10 -4
  21. panther/exceptions.py +24 -2
  22. panther/generics.py +2 -2
  23. panther/main.py +80 -117
  24. panther/middlewares/__init__.py +1 -1
  25. panther/middlewares/base.py +15 -19
  26. panther/middlewares/monitoring.py +42 -0
  27. panther/openapi/__init__.py +1 -0
  28. panther/openapi/templates/openapi.html +27 -0
  29. panther/openapi/urls.py +5 -0
  30. panther/openapi/utils.py +167 -0
  31. panther/openapi/views.py +101 -0
  32. panther/pagination.py +1 -1
  33. panther/panel/middlewares.py +10 -0
  34. panther/panel/templates/base.html +14 -0
  35. panther/panel/templates/create.html +21 -0
  36. panther/panel/templates/create.js +1270 -0
  37. panther/panel/templates/detail.html +55 -0
  38. panther/panel/templates/home.html +9 -0
  39. panther/panel/templates/home.js +30 -0
  40. panther/panel/templates/login.html +47 -0
  41. panther/panel/templates/sidebar.html +13 -0
  42. panther/panel/templates/table.html +73 -0
  43. panther/panel/templates/table.js +339 -0
  44. panther/panel/urls.py +10 -5
  45. panther/panel/utils.py +98 -0
  46. panther/panel/views.py +143 -0
  47. panther/request.py +3 -0
  48. panther/response.py +91 -53
  49. panther/routings.py +7 -2
  50. panther/serializer.py +1 -1
  51. panther/utils.py +34 -26
  52. panther/websocket.py +3 -0
  53. {panther-4.3.7.dist-info → panther-5.0.0b1.dist-info}/METADATA +19 -17
  54. panther-5.0.0b1.dist-info/RECORD +75 -0
  55. {panther-4.3.7.dist-info → panther-5.0.0b1.dist-info}/WHEEL +1 -1
  56. panther-4.3.7.dist-info/RECORD +0 -57
  57. {panther-4.3.7.dist-info → panther-5.0.0b1.dist-info}/entry_points.txt +0 -0
  58. {panther-4.3.7.dist-info → panther-5.0.0b1.dist-info}/licenses/LICENSE +0 -0
  59. {panther-4.3.7.dist-info → panther-5.0.0b1.dist-info}/top_level.txt +0 -0
@@ -1,30 +1,26 @@
1
+ import typing
2
+
3
+ from panther.base_websocket import Websocket
1
4
  from panther.request import Request
2
5
  from panther.response import Response
3
6
  from panther.websocket import GenericWebsocket
4
7
 
5
8
 
6
- class BaseMiddleware:
7
- """Used in both http & ws requests"""
8
- async def before(self, request: Request | GenericWebsocket):
9
- raise NotImplementedError
10
-
11
- async def after(self, response: Response | GenericWebsocket):
12
- raise NotImplementedError
13
-
14
-
15
- class HTTPMiddleware(BaseMiddleware):
9
+ class HTTPMiddleware:
16
10
  """Used only in http requests"""
17
- async def before(self, request: Request):
18
- return request
19
11
 
20
- async def after(self, response: Response):
21
- return response
12
+ def __init__(self, dispatch: typing.Callable):
13
+ self.dispatch = dispatch
14
+
15
+ async def __call__(self, request: Request) -> Response:
16
+ return await self.dispatch(request=request)
22
17
 
23
18
 
24
- class WebsocketMiddleware(BaseMiddleware):
19
+ class WebsocketMiddleware:
25
20
  """Used only in ws requests"""
26
- async def before(self, request: GenericWebsocket):
27
- return request
28
21
 
29
- async def after(self, response: GenericWebsocket):
30
- return response
22
+ def __init__(self, dispatch: typing.Callable):
23
+ self.dispatch = dispatch
24
+
25
+ async def __call__(self, connection: Websocket) -> GenericWebsocket:
26
+ return await self.dispatch(connection=connection)
@@ -0,0 +1,42 @@
1
+ import logging
2
+ from time import perf_counter
3
+
4
+ from panther.base_websocket import Websocket
5
+ from panther.middlewares import HTTPMiddleware, WebsocketMiddleware
6
+ from panther.request import Request
7
+
8
+ logger = logging.getLogger('monitoring')
9
+
10
+
11
+ class MonitoringMiddleware(HTTPMiddleware):
12
+ """
13
+ Create Log Message Like Below:
14
+ date_time | method | path | ip:port | response_time(seconds) | status
15
+ """
16
+
17
+ async def __call__(self, request: Request):
18
+ start_time = perf_counter()
19
+ method = request.scope['method']
20
+
21
+ response = await self.dispatch(request=request)
22
+
23
+ response_time = perf_counter() - start_time # Seconds
24
+ logger.info(f'{method} | {request.path} | {request.client} | {response_time} | {response.status_code}')
25
+ return response
26
+
27
+ class WebsocketMonitoringMiddleware(WebsocketMiddleware):
28
+ """
29
+ Create Log Message Like Below:
30
+ date_time | WS | path | ip:port | connection_time(seconds) | status
31
+ """
32
+ ConnectedConnectionTime = ' - '
33
+
34
+ async def __call__(self, connection: Websocket):
35
+ start_time = perf_counter()
36
+
37
+ logger.info(f'WS | {connection.path} | {connection.client} |{self.ConnectedConnectionTime}| {connection.state}')
38
+ connection = await self.dispatch(connection=connection)
39
+
40
+ connection_time = perf_counter() - start_time # Seconds
41
+ logger.info(f'WS | {connection.path} | {connection.client} | {connection_time} | {connection.state}')
42
+ return connection
@@ -0,0 +1 @@
1
+ from panther.openapi.utils import OutputSchema
@@ -0,0 +1,27 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Swagger UI</title>
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.16.0/swagger-ui.css" />
8
+ </head>
9
+ <body>
10
+ <div id="swagger-ui"></div>
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/4.16.0/swagger-ui-bundle.js"></script>
12
+ <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist/swagger-ui-standalone-preset.js"></script>
13
+ <script>
14
+ const openapiContent = {{ openapi_content | tojson }};
15
+
16
+ const ui = SwaggerUIBundle({
17
+ spec: openapiContent,
18
+ dom_id: '#swagger-ui',
19
+ presets: [
20
+ SwaggerUIBundle.presets.apis,
21
+ SwaggerUIStandalonePreset
22
+ ],
23
+ layout: "StandaloneLayout",
24
+ });
25
+ </script>
26
+ </body>
27
+ </html>
@@ -0,0 +1,5 @@
1
+ from panther.openapi.views import OpenAPI
2
+
3
+ urls = {
4
+ '': OpenAPI,
5
+ }
@@ -0,0 +1,167 @@
1
+ import ast
2
+ import inspect
3
+
4
+ import pydantic
5
+
6
+ from panther import status
7
+ from panther.serializer import ModelSerializer
8
+
9
+
10
+ class EmptyResponseModel(pydantic.BaseModel):
11
+ pass
12
+
13
+
14
+ class OutputSchema:
15
+ """
16
+ Its values only used in OpenAPI response schema
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ model: type[ModelSerializer] | type[pydantic.BaseModel] = EmptyResponseModel,
22
+ status_code: int = status.HTTP_200_OK,
23
+ ):
24
+ self.model = model
25
+ self.status_code = status_code
26
+
27
+
28
+ class ParseEndpoint:
29
+ """
30
+ ParseEndpoint parses the endpoint (function-base/ class-base) and finds where it returns
31
+ and extract the `data` and `status_code` values.
32
+ """
33
+
34
+ def __init__(self, endpoint, method):
35
+ self.tree = ast.parse(inspect.getsource(endpoint))
36
+ self.method = method
37
+
38
+ self.status_code = status.HTTP_200_OK
39
+ self.title = None
40
+ self.data = {}
41
+
42
+ self.parse()
43
+
44
+ def parse(self):
45
+ for branch in self.tree.body:
46
+ match branch:
47
+ case ast.ClassDef(name=name, body=body):
48
+ # class ...(GenericAPI):
49
+ # def get(...
50
+ self.title = name
51
+ for part in body:
52
+ match part:
53
+ case ast.FunctionDef(name=name, body=function_body):
54
+ # def get(...
55
+ if name == self.method:
56
+ self.parse_function(body=function_body)
57
+ break
58
+ case ast.AsyncFunctionDef(name=name, body=function_body):
59
+ # async def get(...
60
+ if name == self.method:
61
+ self.parse_function(body=function_body)
62
+ break
63
+
64
+ case ast.FunctionDef(name=name, body=body):
65
+ # def api(...
66
+ self.title = name
67
+ self.parse_function(body=body)
68
+
69
+ case ast.AsyncFunctionDef(name=name, body=body):
70
+ # async def api(...
71
+ self.title = name
72
+ self.parse_function(body=body)
73
+
74
+ def parse_function(self, body):
75
+ for part in body:
76
+ match part:
77
+ case ast.Return(value=ast.Dict(keys=keys, values=values)):
78
+ # return {...}
79
+ self.status_code = 200
80
+ self.parse_dict_response(keys=keys, values=values)
81
+
82
+ case ast.Return(value=ast.Name(id=name)):
83
+ # return my_data
84
+ self.status_code = 200
85
+ for value in self.searching_variable(body=body, name=name):
86
+ match value:
87
+ # my_data = {...}
88
+ case ast.Dict(keys=keys, values=values):
89
+ self.parse_dict_response(keys=keys, values=values)
90
+
91
+ case ast.Return(value=ast.Call(args=args, keywords=keywords, func=func)):
92
+ if func.id == 'TemplateResponse':
93
+ return
94
+ # return Response(...
95
+ self.parse_response(body=body, args=args, keywords=keywords)
96
+
97
+ def parse_dict_response(self, keys, values):
98
+ for k, v in zip(keys, values):
99
+ final_value = None
100
+ match v:
101
+ case ast.Constant(value=value):
102
+ final_value = value
103
+ self.data[k.value] = final_value
104
+
105
+ def parse_response(self, body, args, keywords):
106
+ for keyword in keywords:
107
+ if keyword.arg == 'data':
108
+ self.parse_data(body=body, value=keyword.value)
109
+ if keyword.arg == 'status_code':
110
+ self.parse_status_code(body=body, value=keyword.value)
111
+
112
+ for i, arg in enumerate(args):
113
+ if i == 0: # index 0 is `data`
114
+ self.parse_data(body=body, value=arg)
115
+ elif i == 1: # index 1 is `status_code`
116
+ self.parse_status_code(body=body, value=arg)
117
+
118
+ def parse_status_code(self, body, value):
119
+ match value:
120
+ # return Response(?, status_code=my_status)
121
+ # return Response(?, my_status)
122
+ case ast.Name():
123
+ for inner_value in self.searching_variable(body=body, name=value.id):
124
+ match inner_value:
125
+ # my_status = status.HTTP_202_ACCEPTED
126
+ case ast.Attribute(value=inner_inner_value, attr=attr):
127
+ if inner_inner_value.id == 'status':
128
+ self.status_code = getattr(status, attr)
129
+ # my_status = 202
130
+ case ast.Constant(value=inner_inner_value):
131
+ self.status_code = inner_inner_value
132
+
133
+ # return Response(?, status_code=status.HTTP_202_ACCEPTED)
134
+ # return Response(?, status.HTTP_202_ACCEPTED)
135
+ case ast.Attribute(value=value, attr=attr):
136
+ if value.id == 'status':
137
+ self.status_code = getattr(status, attr)
138
+ # return Response(?, status_code=202)
139
+ # return Response(?, 202)
140
+ case ast.Constant(value=value):
141
+ self.status_code = value
142
+
143
+ def parse_data(self, body, value):
144
+ match value:
145
+ # return Response(data=my_data, ?)
146
+ # return Response(my_data, ?)
147
+ case ast.Name():
148
+ for value in self.searching_variable(body=body, name=value.id):
149
+ match value:
150
+ # my_data = {...}
151
+ case ast.Dict(keys=keys, values=values):
152
+ self.parse_dict_response(keys=keys, values=values)
153
+
154
+ # return Response(data={...}, ?)
155
+ # return Response({...}, ?)
156
+ case ast.Dict(keys=keys, values=values):
157
+ self.parse_dict_response(keys=keys, values=values)
158
+
159
+ def searching_variable(self, body, name):
160
+ for part in body:
161
+ match part:
162
+ case ast.Assign(targets=targets, value=value):
163
+ for target in targets:
164
+ match target:
165
+ case ast.Name(id=inner_name):
166
+ if inner_name == name:
167
+ yield value
@@ -0,0 +1,101 @@
1
+ import types
2
+
3
+ from panther.app import GenericAPI
4
+ from panther.configs import config
5
+ from panther.openapi.utils import ParseEndpoint
6
+ from panther.response import TemplateResponse
7
+
8
+
9
+ class OpenAPI(GenericAPI):
10
+ @classmethod
11
+ def get_content(cls, endpoint, method):
12
+ parsed = ParseEndpoint(endpoint=endpoint, method=method)
13
+
14
+ if endpoint.output_schema:
15
+ status_code = endpoint.output_schema.status_code
16
+ schema = endpoint.output_schema.model.schema()
17
+ else:
18
+ status_code = parsed.status_code
19
+ schema = {
20
+ 'properties': {
21
+ k: {'default': v} for k, v in parsed.data.items()
22
+ }
23
+ }
24
+
25
+ responses = {}
26
+ if schema:
27
+ responses = {
28
+ 'responses': {
29
+ status_code: {
30
+ 'content': {
31
+ 'application/json': {
32
+ 'schema': schema
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+ request_body = {}
39
+ if endpoint.input_model and method in ['post', 'put', 'patch']:
40
+ request_body = {
41
+ 'requestBody': {
42
+ 'required': True,
43
+ 'content': {
44
+ 'application/json': {
45
+ 'schema': endpoint.input_model.schema() if endpoint.input_model else {}
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ content = {
52
+ 'title': parsed.title,
53
+ 'summary': endpoint.__doc__,
54
+ 'tags': ['.'.join(endpoint.__module__.rsplit('.')[:-1]) or endpoint.__module__],
55
+ } | responses | request_body
56
+ return {method: content}
57
+
58
+ def get(self):
59
+ paths = {}
60
+ # TODO:
61
+ # Try to process the endpoint with `ast` if output_schema is None
62
+ # Create Component for output_schema.model and input_model
63
+ #
64
+ for url, endpoint in config.FLAT_URLS.items():
65
+ if url == '':
66
+ url = '/'
67
+ if not url.startswith('/'):
68
+ url = f'/{url}'
69
+ paths[url] = {}
70
+
71
+ if isinstance(endpoint, types.FunctionType):
72
+ methods = endpoint.methods
73
+ if methods is None or 'POST' in methods:
74
+ paths[url] |= self.get_content(endpoint, 'post')
75
+ if methods is None or 'GET' in methods:
76
+ paths[url] |= self.get_content(endpoint, 'get')
77
+ if methods is None or 'PUT' in methods:
78
+ paths[url] |= self.get_content(endpoint, 'put')
79
+ if methods is None or 'PATCH' in methods:
80
+ paths[url] |= self.get_content(endpoint, 'patch')
81
+ if methods is None or 'DELETE' in methods:
82
+ paths[url] |= self.get_content(endpoint, 'delete')
83
+ else:
84
+ if endpoint.post is not GenericAPI.post:
85
+ paths[url] |= self.get_content(endpoint, 'post')
86
+ if endpoint.get is not GenericAPI.get:
87
+ paths[url] |= self.get_content(endpoint, 'get')
88
+ if endpoint.put is not GenericAPI.put:
89
+ paths[url] |= self.get_content(endpoint, 'put')
90
+ if endpoint.patch is not GenericAPI.patch:
91
+ paths[url] |= self.get_content(endpoint, 'patch')
92
+ if endpoint.delete is not GenericAPI.delete:
93
+ paths[url] |= self.get_content(endpoint, 'delete')
94
+
95
+ openapi_content = {
96
+ 'openapi': '3.0.0',
97
+ 'paths': paths,
98
+ 'components': {}
99
+ }
100
+ print(f'{openapi_content=}')
101
+ return TemplateResponse(name='openapi.html', context={'openapi_content': openapi_content})
panther/pagination.py CHANGED
@@ -11,7 +11,7 @@ class Pagination:
11
11
  'count': 10,
12
12
  'next': '?limit=10&skip=10',
13
13
  'previous': None,
14
- results: [...]
14
+ 'results': [...]
15
15
  }
16
16
  """
17
17
  DEFAULT_LIMIT = 20
@@ -0,0 +1,10 @@
1
+ from panther.middlewares import HTTPMiddleware
2
+ from panther.request import Request
3
+ from panther.response import RedirectResponse
4
+
5
+
6
+ class RedirectToSlashMiddleware(HTTPMiddleware):
7
+ async def __call__(self, request: Request):
8
+ if not request.path.endswith('/'):
9
+ return RedirectResponse(request.path + '/')
10
+ return await self.dispatch(request=request)
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>{{ title }}</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ </head>
10
+ <body class="bg-gray-900 text-white p-8">
11
+ {% block content %}
12
+ {% endblock %}
13
+ </body>
14
+ </html>
@@ -0,0 +1,21 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block content %}
4
+ <div class="max-w-4xl mx-auto">
5
+ <h1 class="text-2xl font-semibold mb-6">Create New Record</h1>
6
+
7
+ <form id="createForm" class="space-y-4 bg-gray-800 p-6 rounded-lg">
8
+ <div id="dynamicInputs" class="space-y-4">
9
+ <!-- Dynamic inputs will be generated here -->
10
+ </div>
11
+ <button type="submit" class="w-full bg-blue-600 hover:bg-blue-500 text-white py-2 px-4 rounded-lg">
12
+ Submit
13
+ </button>
14
+ </form>
15
+ </div>
16
+ <script>
17
+ const isUpdate = false;
18
+ const data = {};
19
+ {% include "create.js" %}
20
+ </script>
21
+ {% endblock %}