Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,23 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Some linters
run: echo "Some linters"
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install linters
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Ruff (format check)
run: ruff format --check

- name: Run Ruff (lint + auto-fix)
run: ruff check

# - name: Run MyPy (static type check)
# run: mypy

test:
uses: ./.github/workflows/tests.yml
Expand Down
19 changes: 17 additions & 2 deletions .github/workflows/other.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,23 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Some linters
run: echo "Some linters"
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install linters
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run Ruff (format check)
run: ruff format --check

- name: Run Ruff (lint + auto-fix)
run: ruff check

# - name: Run MyPy (static type check)
# run: mypy

test:
uses: ./.github/workflows/tests.yml
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
- name: Install dependencies
run: |
pip install --upgrade pip
pip install -r requirements-test.txt
pip install -r requirements.txt

- name: Run tests
run: |
Expand Down
12 changes: 7 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ help:
@echo " make venv - Create virtual environment"
@echo " make install - Install dependencies"
@echo " make freeze - Freeze dependencies"
@echo " make test - Run tests"
@echo " make lint - Lint the code"
@echo " make tests - Run tests"
@echo " make linters - Run ruff formatter and linter"
@echo " make up - Run project"
@echo " make down - Stop project"
@echo " make manage - Run manage.py command"
Expand Down Expand Up @@ -42,11 +42,13 @@ install: venv
freeze: venv
$(PIP) freeze > requirements.txt

test:
tests:
$(COMPOSE) exec -i $(SERVICE) python manage.py test --keepdb

lint:
$(COMPOSE) exec -i $(SERVICE) ruff format --exclude '**/migrations/*.py'
linters:
ruff format; \
ruff check --fix; \
# mypy

migrate:
./migrate.sh
Expand Down
16 changes: 14 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,41 @@ services:
volumes:
- ./workflow:/var/app/workflow
depends_on:
- db
- rabbitmq
migration:
condition: service_completed_successfully
db:
condition: service_started
rabbitmq:
condition: service_started
ports:
- "8001:8080"

worker:
<<: *base
environment:
- DJANGO_SETTINGS_MODULE=workflow_app.settings # remove after change manage.py location
ports: []
command: >
sh -c "celery --app workflow_app worker --concurrency=1 --loglevel=INFO -n $WORKFLOW_WORKER_NAME -Q $WORKFLOW_QUEUES --max-tasks-per-child=1"

scheduler:
<<: *base
environment:
- DJANGO_SETTINGS_MODULE=workflow_app.settings # remove after change manage.py location
ports: []
command: >
sh -c "celery --app workflow_app beat --loglevel=INFO --scheduler workflow.schedulers:DatabaseScheduler --pidfile=/tmp/celerybeat.pid"

migration:
<<: *base
environment:
- DJANGO_SETTINGS_MODULE=workflow_app.settings # remove after change manage.py location
ports: []
depends_on:
db:
condition: service_started
rabbitmq:
condition: service_started
command: ["python", "manage.py", "migrate_all_schemes"]

db:
Expand Down
58 changes: 33 additions & 25 deletions finmars_standardized_errors/formatter.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
import datetime
from dataclasses import asdict
from http import HTTPStatus
from typing import List, Union

import datetime
from django.utils.timezone import now

from rest_framework import exceptions
from rest_framework.status import is_client_error

from .models import ErrorRecord
from .settings import package_settings
from .types import Error, ErrorResponse, ErrorType, ExceptionHandlerContext, ErrorResponseDetails
from .types import Error, ErrorResponse, ErrorResponseDetails, ErrorType, ExceptionHandlerContext


class ExceptionFormatter:
def __init__(
self,
exc: exceptions.APIException,
context: ExceptionHandlerContext,
original_exc: Exception,
self,
exc: exceptions.APIException,
context: ExceptionHandlerContext,
original_exc: Exception,
):
self.exc = exc
self.context = context
Expand All @@ -43,16 +41,24 @@ def run(self):
error_type = self.get_error_type()
errors = self.get_errors()

url = str(self.context['request'].build_absolute_uri())
username = str(self.context['request'].user.username)
url = str(self.context["request"].build_absolute_uri())
username = str(self.context["request"].user.username)
status_code = self.exc.status_code
http_code_to_message = {v.value: v.description for v in HTTPStatus}
message = http_code_to_message[status_code]
error_datetime = str(datetime.datetime.strftime(now(), "%Y-%m-%d %H:%M:%S"))

ErrorRecord.objects.create(url=url, username=username, status_code=self.exc.status_code, message=message, details=asdict(ErrorResponseDetails(error_type, errors)))
ErrorRecord.objects.create(
url=url,
username=username,
status_code=self.exc.status_code,
message=message,
details=asdict(ErrorResponseDetails(error_type, errors)),
)

error_response = self.get_error_response(url, username, status_code, message, error_datetime, error_type, errors)
error_response = self.get_error_response(
url, username, status_code, message, error_datetime, error_type, errors
)

return self.format_error_response(error_response)

Expand All @@ -64,26 +70,32 @@ def get_error_type(self) -> ErrorType:
else:
return ErrorType.SERVER_ERROR

def get_errors(self) -> List[Error]:
def get_errors(self) -> list[Error]:
"""
Account for validation errors in nested serializers by returning a list
of errors instead of a nested dict
"""
return flatten_errors(self.exc.detail)

def get_error_response(self, url: str, username: str, status_code: int, message, error_datetime, error_type: ErrorType, errors: List[Error]):

def get_error_response(
self,
url: str,
username: str,
status_code: int,
message,
error_datetime,
error_type: ErrorType,
errors: list[Error],
):
error_response_details = ErrorResponseDetails(error_type, errors)

return ErrorResponse(url, username, status_code, message, error_datetime, error_response_details)

def format_error_response(self, error_response: ErrorResponse):
return {'error': asdict(error_response)}
return {"error": asdict(error_response)}


def flatten_errors(
detail: Union[list, dict, exceptions.ErrorDetail], attr=None, index=None
) -> List[Error]:
def flatten_errors(detail: list | dict | exceptions.ErrorDetail, attr=None, index=None) -> list[Error]:
"""
convert this:
{
Expand Down Expand Up @@ -129,13 +141,9 @@ def flatten_errors(
new_attr = f"{attr}{package_settings.NESTED_FIELD_SEPARATOR}{index}"
else:
new_attr = str(index)
return flatten_errors(first_item, new_attr, index) + flatten_errors(
rest, attr, index
)
return flatten_errors(first_item, new_attr, index) + flatten_errors(rest, attr, index)
else:
return flatten_errors(first_item, attr, index) + flatten_errors(
rest, attr, index
)
return flatten_errors(first_item, attr, index) + flatten_errors(rest, attr, index)
elif isinstance(detail, dict):
(key, value), *rest = list(detail.items())
if attr:
Expand Down
9 changes: 3 additions & 6 deletions finmars_standardized_errors/handler.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import sys
from typing import Optional

import django
from django.conf import settings
Expand All @@ -17,9 +16,7 @@
from .types import ExceptionHandlerContext


def exception_handler(
exc: Exception, context: ExceptionHandlerContext
) -> Optional[Response]:
def exception_handler(exc: Exception, context: ExceptionHandlerContext) -> Response | None:
exception_handler_class = package_settings.EXCEPTION_HANDLER_CLASS
msg = "`EXCEPTION_HANDLER_CLASS` should be a subclass of ExceptionHandler."
assert issubclass(exception_handler_class, ExceptionHandler), msg
Expand All @@ -31,7 +28,7 @@ def __init__(self, exc: Exception, context: ExceptionHandlerContext):
self.exc = exc
self.context = context

def run(self) -> Optional[Response]:
def run(self) -> Response | None:
"""entrypoint for handling an exception"""
exc = self.convert_known_exceptions(self.exc)
if self.should_not_handle(exc):
Expand Down Expand Up @@ -96,7 +93,7 @@ def get_headers(self, exc: exceptions.APIException) -> dict:
if getattr(exc, "auth_header", None):
headers["WWW-Authenticate"] = exc.auth_header
if getattr(exc, "wait", None):
headers["Retry-After"] = "%d" % exc.wait
headers["Retry-After"] = f"{exc.wait}"
return headers

def report_exception(self, exc: exceptions.APIException, response):
Expand Down
37 changes: 21 additions & 16 deletions finmars_standardized_errors/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@

import logging

_l = logging.getLogger('finmars')
_l = logging.getLogger("finmars")


class ExceptionMiddleware(MiddlewareMixin):

def __init__(self, get_response):
self.get_response = get_response

Expand All @@ -31,7 +30,7 @@ def __call__(self, request):
def process_exception(self, request, exception):
# print('exception %s' % exception)

_l.error("ExceptionMiddleware process error %s" % request.build_absolute_uri())
_l.error("ExceptionMiddleware process error %s", request.build_absolute_uri())
_l.error(traceback.format_exc())

lines = traceback.format_exc().splitlines()[-6:]
Expand All @@ -47,23 +46,29 @@ def process_exception(self, request, exception):
message = http_code_to_message[500]

data = {
'error': {
'url': url,
'username': username,
'details': {
'traceback': '\n'.join(traceback_lines),
'error_message': repr(exception),
"error": {
"url": url,
"username": username,
"details": {
"traceback": "\n".join(traceback_lines),
"error_message": repr(exception),
},
'message': message,
'status_code': 500,
'datetime': str(datetime.datetime.strftime(now(), '%Y-%m-%d %H:%M:%S'))
"message": message,
"status_code": 500,
"datetime": str(datetime.datetime.strftime(now(), "%Y-%m-%d %H:%M:%S")),
}
}

ErrorRecord.objects.create(url=url, username=username, status_code=500, message=message, details={
'traceback': '\n'.join(traceback_lines),
'error_message': repr(exception),
})
ErrorRecord.objects.create(
url=url,
username=username,
status_code=500,
message=message,
details={
"traceback": "\n".join(traceback_lines),
"error_message": repr(exception),
},
)

response_json = json.dumps(data, indent=2, sort_keys=True)

Expand Down
22 changes: 9 additions & 13 deletions finmars_standardized_errors/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,23 @@
from django.db import models
from django.utils.translation import gettext_lazy

class ErrorRecord(models.Model):

url = models.CharField(max_length=255, null=True, blank=True,
verbose_name=gettext_lazy('url'))
class ErrorRecord(models.Model):
url = models.CharField(max_length=255, null=True, blank=True, verbose_name=gettext_lazy("url"))

username = models.CharField(max_length=255, null=True, blank=True,
verbose_name=gettext_lazy('username'))
username = models.CharField(max_length=255, null=True, blank=True, verbose_name=gettext_lazy("username"))

message = models.TextField(blank=True, default='', verbose_name=gettext_lazy('message'))
status_code = models.IntegerField(verbose_name=gettext_lazy('integer'))
message = models.TextField(blank=True, default="", verbose_name=gettext_lazy("message"))
status_code = models.IntegerField(verbose_name=gettext_lazy("integer"))

notes = models.TextField(blank=True, default='', verbose_name=gettext_lazy('notes'))
notes = models.TextField(blank=True, default="", verbose_name=gettext_lazy("notes"))

details_data = models.TextField(null=True, blank=True, verbose_name=gettext_lazy('details data'))
details_data = models.TextField(null=True, blank=True, verbose_name=gettext_lazy("details data"))

created = models.DateTimeField(auto_now_add=True)

class Meta:

ordering = ['-created']

ordering = ["-created"]

@property
def details(self):
Expand All @@ -40,4 +36,4 @@ def details(self, val):
if val:
self.details_data = json.dumps(val, default=str, sort_keys=True)
else:
self.details_data = None
self.details_data = None
Loading