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
5 changes: 1 addition & 4 deletions backend/config/api_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@
import proteins.api.urls
from fpbase.users.api.views import UserViewSet

if settings.DEBUG:
router = DefaultRouter()
else:
router = SimpleRouter()
router = DefaultRouter() if settings.DEBUG else SimpleRouter()

router.register("users", UserViewSet)

Expand Down
29 changes: 16 additions & 13 deletions backend/config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=False)

if READ_DOT_ENV_FILE:
# Operating System Environment variables have precedence over variables defined in the .env file,
# Operating System Environment variables have precedence over variables defined in the
# .env file,
# that is to say variables from the .env files will only be used if not defined
# as environment variables.
env_file = str(ROOT_DIR / ".env")
Expand Down Expand Up @@ -322,7 +323,8 @@
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}

# By Default swagger ui is available only to admin user(s). You can change permission classes to change that
# By Default swagger ui is available only to admin user(s). You can change permission
# classes to change that
# See more configuration options at https://drf-spectacular.readthedocs.io/en/latest/settings.html#settings
SPECTACULAR_SETTINGS = {
"TITLE": "fpbase API",
Expand Down Expand Up @@ -464,17 +466,18 @@ def add_sentry_context(logger, method_name, event_dict):
using the ID to find the full exception context in Sentry.
"""
# Check if sentry_event_id was explicitly passed in extra dict
if "sentry_event_id" not in event_dict:
# Try to get it from Sentry SDK's last_event_id()
# This works if Django/Sentry integration auto-captured an exception
if event_dict.get("exc_info") or "exception" in event_dict:
try:
import sentry_sdk

if event_id := sentry_sdk.last_event_id():
event_dict["sentry_event_id"] = event_id
except (ImportError, AttributeError, Exception):
pass # Sentry not available
# Try to get it from Sentry SDK's last_event_id()
# This works if Django/Sentry integration auto-captured an exception
if "sentry_event_id" not in event_dict and (
event_dict.get("exc_info") or "exception" in event_dict
):
try:
import sentry_sdk

if event_id := sentry_sdk.last_event_id():
event_dict["sentry_event_id"] = event_id
except (ImportError, AttributeError, Exception):
pass # Sentry not available

return event_dict

Expand Down
4 changes: 3 additions & 1 deletion backend/config/settings/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
EMAIL_PORT = 1025

EMAIL_HOST = "localhost"
EMAIL_BACKEND = env("DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend")
EMAIL_BACKEND = env(
"DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend"
)

if env("MAILGUN_API_KEY", default=False) and env("MAILGUN_DOMAIN", default=False):
INSTALLED_APPS += [
Expand Down
6 changes: 4 additions & 2 deletions backend/config/settings/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,9 @@ def WHITENOISE_IMMUTABLE_FILE_TEST(path, url):
send_default_pii=True,
release=HEROKU_SLUG_COMMIT,
traces_sample_rate=env.float("SENTRY_TRACES_SAMPLE_RATE", default=0.1), # 10% of all requests
profiles_sample_rate=env.float("SENTRY_PROFILES_SAMPLE_RATE", default=0.05), # 5% of traced requests
profiles_sample_rate=env.float(
"SENTRY_PROFILES_SAMPLE_RATE", default=0.05
), # 5% of traced requests
)

# Scout APM Configuration
Expand Down Expand Up @@ -322,7 +324,7 @@ def WHITENOISE_IMMUTABLE_FILE_TEST(path, url):

# Enable API and GraphQL rate limiting in production
REST_FRAMEWORK["DEFAULT_THROTTLE_CLASSES"] = [
"fpbase.views.SameOriginExemptAnonThrottle", # Custom throttle that exempts same-origin requests
"fpbase.views.SameOriginExemptAnonThrottle", # Custom throttle that exempts same-origin
"rest_framework.throttling.UserRateThrottle",
]
# NOTE: Rate limiting settings are used by both REST API and GraphQL
Expand Down
1 change: 0 additions & 1 deletion backend/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@
TemplateView.as_view(template_name="pages/bleaching.html"),
name="bleaching",
),
# path('mutations/', TemplateView.as_view(template_name='pages/mutations.html'), name='mutations'),
path(
"robots.txt",
TemplateView.as_view(template_name="robots.txt", content_type="text/plain"),
Expand Down
8 changes: 6 additions & 2 deletions backend/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,12 @@ def mock_get_organism_info(organism_id: str | int):
}

# Use unittest.mock.patch for session-scoped mocking
patcher1 = unittest.mock.patch("proteins.extrest.entrez.doi_lookup", side_effect=mock_doi_lookup)
patcher2 = unittest.mock.patch("proteins.extrest.entrez.get_organism_info", side_effect=mock_get_organism_info)
patcher1 = unittest.mock.patch(
"proteins.extrest.entrez.doi_lookup", side_effect=mock_doi_lookup
)
patcher2 = unittest.mock.patch(
"proteins.extrest.entrez.get_organism_info", side_effect=mock_get_organism_info
)

patcher1.start()
patcher2.start()
Expand Down
4 changes: 3 additions & 1 deletion backend/favit/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ def get_favorite(self, user, obj, model=None):
return None

try:
return self.get_query_set().get(user=user, target_content_type=content_type, target_object_id=obj.id)
return self.get_query_set().get(
user=user, target_content_type=content_type, target_object_id=obj.id
)
except self.model.DoesNotExist:
return None

Expand Down
4 changes: 3 additions & 1 deletion backend/favit/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ class Favorite(models.Model):
on_delete=models.CASCADE,
)
target_content_type_id: int
target_content_type: models.ForeignKey[ContentType] = models.ForeignKey(ContentType, on_delete=models.CASCADE)
target_content_type: models.ForeignKey[ContentType] = models.ForeignKey(
ContentType, on_delete=models.CASCADE
)
target_object_id = models.PositiveIntegerField()
target = GenericForeignKey("target_content_type", "target_object_id")
timestamp = models.DateTimeField(auto_now_add=True, db_index=True)
Expand Down
4 changes: 3 additions & 1 deletion backend/fpbase/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
from fpbase.celery import app as celery_app

__version__ = "0.1.0"
__version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace("-", ".", 1).split(".")])
__version_info__ = tuple(
[int(num) if num.isdigit() else num for num in __version__.replace("-", ".", 1).split(".")]
)


__all__ = ("celery_app",)
4 changes: 3 additions & 1 deletion backend/fpbase/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ def login_required_message_and_redirect(
message=default_message,
):
if function:
return login_required_message(login_required(function, redirect_field_name, login_url), message)
return login_required_message(
login_required(function, redirect_field_name, login_url), message
)

return lambda deferred_function: login_required_message_and_redirect(
deferred_function, redirect_field_name, login_url, message
Expand Down
3 changes: 2 additions & 1 deletion backend/fpbase/templatetags/fpbase_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
ICON_DIR = Path(__file__).parent.parent / "static" / "icons"
if not ICON_DIR.exists():
raise RuntimeError(
f"Icon directory does not exist: {ICON_DIR}. Please run 'python scripts/extract_fa_icons.py' to generate it."
f"Icon directory does not exist: {ICON_DIR}. Please run "
"'python scripts/extract_fa_icons.py' to generate it."
)


Expand Down
4 changes: 3 additions & 1 deletion backend/fpbase/users/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ def get_queryset(self, request):
super()
.get_queryset(request)
.prefetch_related("socialaccount_set", "proteincollections", "emailaddress_set")
.annotate(verified=Exists(EmailAddress.objects.filter(user_id=OuterRef("id"), verified=True)))
.annotate(
verified=Exists(EmailAddress.objects.filter(user_id=OuterRef("id"), verified=True))
)
.annotate(
_collections=Count("proteincollections"),
_microscopes=Count("microscopes"),
Expand Down
4 changes: 3 additions & 1 deletion backend/fpbase/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ class UserLogin(models.Model):
"""Represent users' logins, one per record"""

user_id: int
user: models.ForeignKey[User] = models.ForeignKey(User, on_delete=models.CASCADE, related_name="logins")
user: models.ForeignKey[User] = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="logins"
)
timestamp = models.DateTimeField(auto_now_add=True)

def __str__(self) -> str:
Expand Down
3 changes: 2 additions & 1 deletion backend/fpbase/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ def allow_request(self, request, view):
# Check if this is a same-origin request by comparing the referer with the host
referer = request.headers.get("referer", "")

# If the referer contains our host (accounting for port differences), it's a same-origin request
# If the referer contains our host (accounting for port differences), it's a
# same-origin request
# Note: This checks for the host in the referer URL (e.g., "https://www.fpbase.org/...")
# We strip the port from host comparison to handle localhost:8000 vs fpbase.org
if referer:
Expand Down
4 changes: 3 additions & 1 deletion backend/fpseq/align.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,9 @@ def __str__(self):
out = []
for t, q in zip(a, b):
out.append(q)
out.append("".join(["*" if x != y else (" " if x == " " else "|") for x, y in zip(t, q)]))
out.append(
"".join(["*" if x != y else (" " if x == " " else "|") for x, y in zip(t, q)])
)
out.append(t + "\n")
return "\n".join(out)

Expand Down
38 changes: 23 additions & 15 deletions backend/fpseq/mutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
(?P<new_chars>[{0}*]+)?
(?:ext(?P<ext>[{0}]+))?
(?:,|$|/|\s)""".format(r"A-Z*", "{1}"),
re.X,
re.VERBOSE,
)


Expand Down Expand Up @@ -129,7 +129,9 @@ def __init__(
if self.operation == "sub" and (stop_char or self.stop_idx):
raise ValueError("Substitution mutations cannot specify a range (or a stop_char/idx)")
if stop_idx and (int(stop_idx) < int(start_idx)):
raise ValueError(f"Stop position ({stop_idx}) must be greater than start position ({start_idx})")
raise ValueError(
f"Stop position ({stop_idx}) must be greater than start position ({start_idx})"
)
if self.operation.endswith("ins"):
if not (stop_char and self.stop_idx):
print(stop_char)
Expand Down Expand Up @@ -171,9 +173,7 @@ def __repr__(self):
return f"<Mutation: {self}>"

def __eq__(self, other):
if str(self) == str(other):
return True
return False
return str(self) == str(other)

def __hash__(self):
return hash(str(self))
Expand All @@ -185,7 +185,9 @@ def __call__(self, seq: Sequence, idx0: int = 1) -> tuple[Sequence, int]:
# self._assert_position_consistency(seq, idx0)
startpos = self.start_idx - idx0
if startpos > len(seq): # allowing one extra position for extensions
raise IndexError(f"Starting position {self.start_idx} is outside of sequence with length {len(seq)}")
raise IndexError(
f"Starting position {self.start_idx} is outside of sequence with length {len(seq)}"
)
if self.operation == "sub":
end = startpos + 1
return (seq[:startpos] + self.new_chars + seq[end:], 0)
Expand Down Expand Up @@ -230,7 +232,7 @@ def _assert_position_consistency(self, seq, idx0=1):
)
if self.stop_idx and self.stop_char:
stoppos = self.stop_idx - idx0
if not seq[stoppos] == self.stop_char:
if seq[stoppos] != self.stop_char:
beg = stoppos - 3
beg2 = stoppos + 1
end = stoppos + 4
Expand All @@ -248,7 +250,10 @@ def from_str(cls, mutstring, sep="/"):
if not m:
raise ValueError(f"Mutation code invalid: {mutstring}")
if len(m) > 1:
raise ValueError("Multiple mutation codes found. For multiple mutations, create a MutationSet instead")
raise ValueError(
"Multiple mutation codes found. For multiple mutations, "
"create a MutationSet instead"
)
return m[0]


Expand All @@ -271,7 +276,9 @@ def clear_insertions(ins_start_idx, insertions, extension=False):
if extension:
out.append(f"*{ins_start_idx + 1}{insertions[0]}ext{insertions[1:]}")
else:
out.append(f"{ins_start_char}{ins_start_idx}_{before}{ins_start_idx + 1}ins{insertions}")
out.append(
f"{ins_start_char}{ins_start_idx}_{before}{ins_start_idx + 1}ins{insertions}"
)

def clear_deletions(delstart, numdel, delins, idx):
string = "{}{}del".format(delstart, f"_{lastchar + str(idx - 1)}" if numdel > 1 else "")
Expand Down Expand Up @@ -366,7 +373,9 @@ def __init__(self, muts=None, position_labels=None):
mut.stop_label = position_labels[mut.stop_idx - 1]
else:
try:
mut.start_label = str(len(position_labels) - mut.start_idx + int(position_labels[-1]))
mut.start_label = str(
len(position_labels) - mut.start_idx + int(position_labels[-1])
)
except Exception:
mut.start_label = str(mut.start_idx)

Expand Down Expand Up @@ -545,7 +554,9 @@ def __eq__(self, other):
try:
otherm = MutationSet(other).muts
except Exception as e:
raise ValueError(f"Could not compare MutationSet object with other: {other}") from e
raise ValueError(
f"Could not compare MutationSet object with other: {other}"
) from e
if not otherm:
raise ValueError(f"operation not valid between type MutationSet and {type(other)}")
else:
Expand Down Expand Up @@ -647,10 +658,7 @@ def rand_mut(seq):
elif operation in ("ins", "delins"):
new_chars = "".join(choices(AAs, k=randint(1, 6)))
if operation in ("del", "ins", "delins"):
if operation == "ins":
stop_idx = start_idx + 1
else:
stop_idx = start_idx + randint(1, 6)
stop_idx = start_idx + 1 if operation == "ins" else start_idx + randint(1, 6)
while stop_idx > len(seq) - 2:
stop_idx = start_idx + randint(0, 6)
stop_char = seq[stop_idx - 1]
Expand Down
15 changes: 11 additions & 4 deletions backend/fpseq/skbio_protein.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ def _munge_to_sequence(self, other, method):
if isinstance(other, SkbSequence):
if type(other) is not type(self):
raise TypeError(
f"Cannot use {self.__class__.__name__} and {other.__class__.__name__} together with `{method}`"
f"Cannot use {self.__class__.__name__} and "
f"{other.__class__.__name__} together with `{method}`"
)
else:
return other
Expand Down Expand Up @@ -214,9 +215,14 @@ def __getitem__(self, indexable):
elif isinstance(indexable, str | bool):
raise IndexError(f"Cannot index with {type(indexable).__name__} type: {indexable!r}")

if isinstance(indexable, np.ndarray) and indexable.dtype == bool and len(indexable) != len(self):
if (
isinstance(indexable, np.ndarray)
and indexable.dtype == bool
and len(indexable) != len(self)
):
raise IndexError(
f"An boolean vector index must be the same length as the sequence ({len(self)}, not {len(indexable)})."
f"An boolean vector index must be the same length as the sequence "
f"({len(self)}, not {len(indexable)})."
)

if isinstance(indexable, np.ndarray) and indexable.size == 0:
Expand Down Expand Up @@ -270,7 +276,8 @@ def _gap_codes(cls):
def _validate(self):
"""https://github.com/biocore/scikit-bio/blob/0.5.4/skbio/sequence/_grammared_sequence.py#L340"""
invalid_characters = (
np.bincount(self._bytes, minlength=self._number_of_extended_ascii_codes) * self._validation_mask
np.bincount(self._bytes, minlength=self._number_of_extended_ascii_codes)
* self._validation_mask
)
if np.any(invalid_characters):
bad = list(np.where(invalid_characters > 0)[0].astype(np.uint8).view("|S1"))
Expand Down
11 changes: 6 additions & 5 deletions backend/proteins/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,10 +284,9 @@ def owner(self, obj):
owner = obj.owner
# FluorState is a base class - resolve to the actual subclass admin
if isinstance(owner, FluorState):
if owner.entity_type == FluorState.EntityTypes.PROTEIN:
model_name = "state"
else:
model_name = "dyestate"
model_name = (
"state" if owner.entity_type == FluorState.EntityTypes.PROTEIN else "dyestate"
)
else:
model_name = owner._meta.model.__name__.lower()

Expand Down Expand Up @@ -724,7 +723,9 @@ class Meta:
fields = ("protein", "parent", "mutation", "root_node", "rootmut")
readonly_fields = "root_node"

parent = forms.ModelChoiceField(required=False, queryset=Lineage.objects.prefetch_related("protein").all())
parent = forms.ModelChoiceField(
required=False, queryset=Lineage.objects.prefetch_related("protein").all()
)
root_node = forms.ModelChoiceField(queryset=Lineage.objects.prefetch_related("protein").all())


Expand Down
Loading
Loading