Skip to content

Commit

Permalink
Improve MongoDB Connection & Queries #81
Browse files Browse the repository at this point in the history
  • Loading branch information
AliRn76 authored Feb 17, 2024
2 parents b5f07bb + 9018cf3 commit ba80d6d
Show file tree
Hide file tree
Showing 45 changed files with 1,005 additions and 745 deletions.
6 changes: 2 additions & 4 deletions .github/workflows/pull_request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,8 @@ jobs:
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Install pymongo
run: |
pip uninstall -y bson
pip install pymongo
- name: Install motor
run: pip install motor

- name: Start MongoDB
uses: supercharge/[email protected]
Expand Down
13 changes: 5 additions & 8 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,8 @@ jobs:
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Install pymongo
run: |
pip uninstall -y bson
pip install pymongo
- name: Install motor
run: pip install motor

- name: Start MongoDB
uses: supercharge/[email protected]
Expand Down Expand Up @@ -135,10 +133,9 @@ jobs:
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Install pymongo
run: |
pip uninstall -y bson
pip install pymongo
- name: Install motor
run: pip install pymongo

- name: Start MongoDB
uses: supercharge/[email protected]
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/authentications.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ JWTConfig = {
- Create a class and inherits it from `panther.authentications.BaseAuthentication`


- Implement `authentication(cls, request: Request)` method
- Implement `async authentication(cls, request: Request)` method
- Process the `request.headers.authorization` or ...
- Return Instance of `USER_MODEL`
- Or raise `panther.exceptions.AuthenticationException`
Expand Down
1 change: 1 addition & 0 deletions docs/docs/release_notes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
### 4.0.0
- Move `database` and `redis` connections from `MIDDLEWARES` to their own block, `DATABASE` and `REDIS`
- Make queries `async`
- Add `login()` & `logout()` to `JWTAuthentication` and used it in `BaseUser`
- Support `Authentication` & `Authorization` in `Websocket`
- Rename all exceptions suffix from `Exception` to `Error` (https://peps.python.org/pep-0008/#exception-names)
Expand Down
19 changes: 11 additions & 8 deletions example/app/apis.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,24 +114,27 @@ async def rate_limit():

@API(input_model=UserUpdateSerializer)
async def patch_user(request: Request):
_, user = User.find_or_insert(username='Ali', password='1', age=12)
_, user = await User.find_or_insert(username='Ali', password='1', age=12)
user.update(**request.validated_data.model_dump())
return Response(data=user)


class PatchUser(GenericAPI):
input_model = UserUpdateSerializer

def patch(self, request: Request, *args, **kwargs):
_, user = User.find_or_insert(username='Ali', password='1', age=12)
user.update(**request.validated_data.model_dump())
async def patch(self, request: Request, *args, **kwargs):
_, user = await User.find_or_insert(username='Ali', password='1', age=12)
await user.update(**request.validated_data.model_dump())
return Response(data=user)


@API()
async def single_user(request: Request):
user = User.insert_one(username='Ali', password='1', age=12)
return Response(data=user, status_code=200)
# user = await User.insert_one(username='Ali', password='1', age=12)
# users = await User.find({'$where': 'function() { sleep(300); return true; }'})
users = await User.find()
# print(users.limit(2))
return Response(data=users[:3], status_code=200)


# @API(input=UserInputSerializer, output_model=UserSerializer)
Expand Down Expand Up @@ -202,9 +205,9 @@ def post(self, request: Request, *args, **kwargs):


@API()
def login_api():
async def login_api():
_, user = User.find_or_insert(username='fake-username', password='secret-password')
return user.login()
return await user.login()


@API(auth=True)
Expand Down
8 changes: 7 additions & 1 deletion example/app/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
from panther.db.models import BaseUser


class User(BaseUser):
class CustomQuery:
@classmethod
def find_last(cls):
return cls.last()


class User(BaseUser, CustomQuery):
username: str
password: str
1 change: 0 additions & 1 deletion example/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,5 @@ async def test(*args, **kwargs):
'bg-tasks/': run_background_tasks_api,
'custom-response/': custom_response_class_api,
'image/': ImageAPI,
'login/': login_api,
'logout/': logout_api,
}
6 changes: 3 additions & 3 deletions example/core/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@

DATABASE = {
'engine': {
# 'class': 'panther.db.connections.MongoDBConnection',
'class': 'panther.db.connections.PantherDBConnection',
# 'host': f'mongodb://{DB_HOST}:27017/{DB_NAME}'
'class': 'panther.db.connections.MongoDBConnection',
# 'class': 'panther.db.connections.PantherDBConnection',
'host': f'mongodb://{DB_HOST}:27017/{DB_NAME}'
},
# 'query': ...,
}
Expand Down
2 changes: 1 addition & 1 deletion example/core/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

class UserPermission(BasePermission):
@classmethod
def authorization(cls, request: Request) -> bool:
async def authorization(cls, request: Request) -> bool:
if request.user.username == 'Ali':
return True
return False
13 changes: 1 addition & 12 deletions panther/_load_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def load_redis(_configs: dict, /) -> None:
# We have to create another dict then pop the 'class' else we can't pass the tests
args = redis_config.copy()
args.pop('class', None)
config['redis'] = redis_class(**args, init=True)
redis_class(**args, init=True)


def load_startup(_configs: dict, /) -> None:
Expand Down Expand Up @@ -126,21 +126,10 @@ def load_log_queries(_configs: dict, /) -> None:


def load_middlewares(_configs: dict, /) -> None:
"""
Should be after `load_database()`
Collect The Middlewares & Set `DatabaseMiddleware` If it is needed.
"""
from panther.middlewares import BaseMiddleware

middlewares = {'http': [], 'ws': []}

# Add Database Middleware
if config['database']:
database_middleware = import_class('panther.middlewares.db.DatabaseMiddleware')
middlewares['http'].append(database_middleware())
middlewares['ws'].append(database_middleware())

# Collect Middlewares
for middleware in _configs.get('MIDDLEWARES') or []:
if not isinstance(middleware, list | tuple):
Expand Down
12 changes: 6 additions & 6 deletions panther/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,13 @@ async def wrapper(request: Request) -> Response:
raise MethodNotAllowedAPIError

# 2. Authentication
self.handle_authentications()
await self.handle_authentications()

# 3. Throttling
self.handle_throttling()

# 4. Permissions
self.handle_permissions()
await self.handle_permissions()

# 5. Validate Input
if self.request.method in ['POST', 'PUT', 'PATCH']:
Expand Down Expand Up @@ -109,13 +109,13 @@ async def wrapper(request: Request) -> Response:

return wrapper

def handle_authentications(self) -> None:
async def handle_authentications(self) -> None:
auth_class = config['authentication']
if self.auth:
if not auth_class:
logger.critical('"AUTHENTICATION" has not been set in configs')
raise APIError
user = auth_class.authentication(self.request)
user = await auth_class.authentication(self.request)
self.request.user = user

def handle_throttling(self) -> None:
Expand All @@ -128,12 +128,12 @@ def handle_throttling(self) -> None:

throttling_storage[throttling_key] += 1

def handle_permissions(self) -> None:
async def handle_permissions(self) -> None:
for perm in self.permissions:
if type(perm.authorization).__name__ != 'method':
logger.error(f'{perm.__name__}.authorization should be "classmethod"')
raise AuthorizationAPIError
if perm.authorization(self.request) is False:
if await perm.authorization(self.request) is False:
raise AuthorizationAPIError

def handle_input_validation(self):
Expand Down
10 changes: 5 additions & 5 deletions panther/authentications.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
class BaseAuthentication:
@classmethod
@abstractmethod
def authentication(cls, request: Request):
async def authentication(cls, request: Request):
"""Return Instance of User"""
msg = f'{cls.__name__}.authentication() is not implemented.'
raise cls.exception(msg) from None
Expand All @@ -48,7 +48,7 @@ def get_authorization_header(cls, request: Request) -> str:
raise cls.exception(msg) from None

@classmethod
def authentication(cls, request: Request) -> Model:
async def authentication(cls, request: Request) -> Model:
auth_header = cls.get_authorization_header(request).split()

if len(auth_header) != 2:
Expand All @@ -71,7 +71,7 @@ def authentication(cls, request: Request) -> Model:
raise cls.exception(msg) from None

payload = cls.decode_jwt(token)
user = cls.get_user(payload)
user = await cls.get_user(payload)
user._auth_token = token
return user

Expand All @@ -88,14 +88,14 @@ def decode_jwt(cls, token: str) -> dict:
raise cls.exception(e) from None

@classmethod
def get_user(cls, payload: dict) -> Model:
async def get_user(cls, payload: dict) -> Model:
"""Get UserModel from config, else use default UserModel from cls.model"""
if (user_id := payload.get('user_id')) is None:
msg = 'Payload does not have `user_id`'
raise cls.exception(msg)

user_model = config['user_model'] or cls.model
if user := user_model.find_one(id=user_id):
if user := await user_model.find_one(id=user_id):
return user

msg = 'User not found'
Expand Down
4 changes: 2 additions & 2 deletions panther/base_websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ async def handle_authentication(cls, connection: Websocket) -> bool:
await connection.close(reason='Authentication Error')
return True
try:
connection.user = config.ws_authentication.authentication(connection)
connection.user = await config.ws_authentication.authentication(connection)
except AuthenticationAPIError as e:
await connection.close(reason=e.detail)
return False
Expand All @@ -164,7 +164,7 @@ async def handle_permissions(cls, connection: Websocket) -> bool:
logger.error(f'{perm.__name__}.authorization should be "classmethod"')
await connection.close(reason='Permission Denied')
return True
if perm.authorization(connection) is False:
if await perm.authorization(connection) is False:
await connection.close(reason='Permission Denied')
return True
return False
Expand Down
7 changes: 5 additions & 2 deletions panther/cli/utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import logging
import platform

from rich import print as rprint

from panther.exceptions import PantherError

logger = logging.getLogger('panther')


if platform.system() == 'Windows':
h = '|'
v = '_'
Expand Down Expand Up @@ -109,11 +109,14 @@ def print_uvicorn_help_message():


def print_info(config: dict):
from panther.db.connections import redis


mo = config['monitoring']
lq = config['log_queries']
bt = config['background_tasks']
ws = config['has_ws']
rd = getattr(config['redis'], 'is_connected', False)
rd = redis.is_connected
bd = '{0:<39}'.format(str(config['base_dir']))
if len(bd) > 39:
bd = f'{bd[:36]}...'
Expand Down
2 changes: 0 additions & 2 deletions panther/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ class Config(Singleton):
pantherdb_encryption: bool
query_engine: typing.Callable | None
database: typing.Callable | None
redis: typing.Callable | None

def __setattr__(self, key, value):
super().__setattr__(key, value)
Expand Down Expand Up @@ -118,7 +117,6 @@ def refresh(self):
'pantherdb_encryption': False,
'query_engine': None,
'database': None,
'redis': None,
}

config = Config(**default_configs)
Loading

0 comments on commit ba80d6d

Please sign in to comment.