Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1735d8c
more recent docker compose
bryan-brancotte Sep 29, 2025
f506003
wip on restore doc
bryan-brancotte Sep 29, 2025
a53f750
don't serve https, cleanup
bryan-brancotte Sep 29, 2025
2c83f4c
rework service to improve readability, use fork and actively wait for…
bryan-brancotte Feb 4, 2026
58e75d1
provide default env var
bryan-brancotte Feb 5, 2026
e575ba7
prune on demande
bryan-brancotte Feb 5, 2026
0e28a9b
start on the same process, keep log in service
bryan-brancotte Feb 5, 2026
c1377e9
update django
bryan-brancotte Feb 5, 2026
9f0da8d
update db
bryan-brancotte Feb 5, 2026
14eec11
update dockerfile
bryan-brancotte Feb 5, 2026
5bf2e4d
explicitly install pytz
bryan-brancotte Feb 5, 2026
73d38fb
use python 3.12
bryan-brancotte Feb 5, 2026
dee0141
import procedure compatible with prod
bryan-brancotte Feb 5, 2026
6f17e62
quiet shell, use --no-imports as model are not needed for getting STA…
bryan-brancotte Feb 5, 2026
6d7f3af
use asgi with uvicorn
bryan-brancotte Feb 6, 2026
25b04c1
change DEBUG var name to not clash with inotify
bryan-brancotte Feb 6, 2026
3309678
more ALLOWED_HOSTS
bryan-brancotte Feb 6, 2026
3816281
use http when running docker compose in dev
bryan-brancotte Feb 6, 2026
3b0f3c0
add uvicorn/inotify requirements
bryan-brancotte Feb 6, 2026
77abf13
indicate entrypoint in dockerfile
bryan-brancotte Feb 6, 2026
ad1e361
add DJANGO_DEBUG
bryan-brancotte Feb 6, 2026
b959606
add dev theme
bryan-brancotte Feb 6, 2026
d5a72e4
no need for settings.ini if running docker-compose.dev.yaml
bryan-brancotte Feb 6, 2026
0d6637b
test api endpoints
bryan-brancotte Feb 6, 2026
fa23660
add example for specific tests
bryan-brancotte Feb 6, 2026
eb8235b
cache more calls
bryan-brancotte Feb 9, 2026
50b2be5
keep django-jazzmin in the former css
bryan-brancotte Mar 2, 2026
1af377f
Merge branch 'master' into bump-dependencies
bryan-brancotte Mar 10, 2026
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
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
local.ini
source_info.json
./import_data/
./dumps/

#END OF FILE
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ settings.ini
# folder where data are git cloned
./import_data
import_data
./dumps

# Virtual Studio Code settings
.vscode
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,16 @@ docker compose -f docker-compose.yaml -f docker-compose.dev.yaml down --rmi all

## How to do a dump
```sh
docker exec -e PGPASSWORD=the_super_password $(docker ps -q) pg_dump --clean -h localhost -U postgres --format plain | sed "s/pbkdf2_sha256[^\t]*/redacted/g" > my_dump.sql
docker exec -e PGPASSWORD=the_super_password ifbcatsrc_db_1 pg_dump --clean -h localhost -U postgres --format plain | sed "s/pbkdf2_sha256[^\t]*/redacted/g" > my_dump.sql
```


## How to retore a db dump
## How to restore a db dump
We consider here that no container are started. You have to get the dump, and uncompress it in the root directory of the project, and name it `data`
```sh
docker stop $(docker ps -q)
docker compose -f docker-compose.yaml -f docker-compose.dev.yaml run -d db
docker exec -e PGPASSWORD=the_super_password $(docker ps -q) psql -h localhost -U postgres -f /code/data
docker compose -f docker-compose.yaml -f docker-compose.import.yaml run -d db
docker exec $(docker ps -q) psql -h localhost -U postgres -f /code/dumps/latest.sql
```

# How to manage the server
Expand Down
8 changes: 5 additions & 3 deletions django.dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.9-bullseye
FROM python:3.12

RUN apt-get update && \
apt-get install -y \
Expand All @@ -10,18 +10,20 @@ RUN apt-get update && \
&& rm -rf /var/lib/apt/lists/* \
&& python -m pip install --upgrade pip

ENV PYTHONUNBUFFERED 1
ENV PYTHONUNBUFFERED=1
EXPOSE 8000
RUN mkdir /code
WORKDIR /code
CMD ["gunicorn", "--reload", "--reload-engine", "inotify", "--chdir", "ifbcat", "--bind", ":8000", "ifbcat.wsgi:application"]
CMD ["gunicorn", "--bind", ":8000", "ifbcat.asgi:application", "-k", "uvicorn.workers.UvicornWorker"]

COPY requirements.txt /code/
RUN python -m pip install --upgrade pip && pip install -r requirements.txt

COPY ./resources/*-entrypoint.sh /
RUN chmod a+x /*-entrypoint.sh

ENTRYPOINT ["/docker-entrypoint.sh"]

COPY . /code/

ARG CI_COMMIT_SHA
Expand Down
37 changes: 29 additions & 8 deletions docker-compose.dev.yaml
Original file line number Diff line number Diff line change
@@ -1,23 +1,44 @@
version: '3'

services:
db:
volumes:
- .:/code # for dev purpose only !!!
- ifbcat-dev-db-data:/var/lib/postgresql/data
- ifbcat-dev-db-data:/var/lib/postgresql

web:
command:
- "gunicorn"
- "--reload"
- "--reload-engine"
- "inotify"
- "--chdir"
- "ifbcat"
- "--bind"
- ":8000"
- "ifbcat.asgi:application"
- "-k"
- "uvicorn.workers.UvicornWorker"
environment:
- NGINX_FROWARD_PROTO=http
- POSTGRES_PASSWORD=the_super_password
- DJANGO_DEBUG=True
- IS_DEV_INSTANCE=True
volumes:
- .:/code # for dev purpose only !!!
- ./import_data:/code/import_data # for dev purpose only !!!


nginx:
environment:
- NGINX_FROWARD_PROTO=http



# The Database manager (adminer)
adminer:
image: adminer
restart: always
ports:
- "8081:8080"
# adminer:
# image: adminer
# restart: always
# ports:
# - "8081:8080"

volumes:
ifbcat-dev-db-data:
6 changes: 4 additions & 2 deletions docker-compose.import.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
version: '3'

services:
db:
volumes:
- .:/code # for dev purpose only !!!

web:
restart:
on-failure
Expand Down
28 changes: 7 additions & 21 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
version: '3'

services:

db:
image: postgres:12
image: postgres:18
volumes:
- /var/ifbcat/db_data:/var/lib/postgresql/data
- /var/ifbcat/db_data:/var/lib/postgresql
restart: always
ports:
- "5433:5432"
Expand All @@ -19,11 +17,9 @@ services:
dockerfile: django.dockerfile
restart:
always
entrypoint: /docker-entrypoint.sh
command: gunicorn --reload ifbcat.wsgi -b 0.0.0.0:8000 --threads 2
environment:
- CI_COMMIT_SHA=$CI_COMMIT_SHA
- CI_COMMIT_DATE=$CI_COMMIT_DATE
- CI_COMMIT_SHA=${CI_COMMIT_SHA:-xx}
- CI_COMMIT_DATE=${CI_COMMIT_DATE:-xx}
env_file:
- ./resources/default.ini
- ./local.ini
Expand All @@ -37,21 +33,11 @@ services:
nginx:
image: nginx:1.19-alpine
volumes:
- ./resources/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./resources/nginx.conf.template:/etc/nginx/templates/default.conf.template:ro
- /var/ifbcat/static:/static:ro
ports:
- "8080:8080"
depends_on:
- web

nginx-https:
image: nginx:1.19-alpine
volumes:
- ./resources/nginx-https.conf:/etc/nginx/conf.d/default.conf:ro
- /var/ifbcat/static:/static:ro
- /etc/ssl/certs/wildcard-chained-france-bioinformatique.crt:/wildcard-chained-france-bioinformatique.crt:ro
- /etc/ssl/private/wildcard-france-bioinformatique.key:/wildcard-france-bioinformatique.key:ro
ports:
- "443:8443"
environment:
- NGINX_FROWARD_PROTO=https
depends_on:
- web
16 changes: 16 additions & 0 deletions ifbcat/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
WSGI config for ifbcat project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ifbcat.settings')

application = get_asgi_application()
3 changes: 2 additions & 1 deletion ifbcat/settings.example.ini
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[settings]
POSTGRES_PASSWORD=the_super_password
DEBUG=True
DJANGO_DEBUG=True
IS_DEV_INSTANCE=True
4 changes: 3 additions & 1 deletion ifbcat/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
SECRET_KEY = config('SECRET_KEY', '#1eg!-*r&1*y8*s9!g^!if!-(1&11k0%*7b$-jwgv!7u!ae7wt')

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config('DEBUG', 'true').lower() == 'true'
DEBUG = config('DJANGO_DEBUG', 'true').lower() == 'true'

IS_DEV_INSTANCE = config('IS_DEV_INSTANCE', 'false').lower() == 'true'

try:
ALLOWED_HOSTS = [s.strip() for s in config('ALLOWED_HOSTS', '127.0.0.1').split(',')]
Expand Down
4 changes: 2 additions & 2 deletions ifbcat_api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ def _action(modeladmin, request, qset):
messages.SUCCESS,
)

name = 'revoke_%s' % re.sub('[\W]+', '_', str(group))
name = 'revoke_%s' % re.sub(r'[\W]+', '_', str(group))
return name, (_action, name, 'Revoke permissions "%s"' % group)

@staticmethod
Expand Down Expand Up @@ -380,7 +380,7 @@ def _action(modeladmin, request, qset):
messages.SUCCESS,
)

name = 'grant_%s' % re.sub('[\W]+', '_', str(group))
name = 'grant_%s' % re.sub(r'[\W]+', '_', str(group))
return name, (_action, name, 'Grant permissions "%s"' % group)

@staticmethod
Expand Down
43 changes: 43 additions & 0 deletions ifbcat_api/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,49 @@
from django.db.models import ManyToManyRel, ManyToOneRel
from opencage.geocoder import OpenCageGeocode
from rest_framework import serializers
import os
import json
import functools
from typing import Any, Callable


def disk_cache(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs) -> Any:
cache_base = os.environ.get('CACHE_DIR')
if not cache_base:
return func(*args, **kwargs)
cache_dir = os.path.join(cache_base, func.__name__)
os.makedirs(cache_dir, exist_ok=True)

a_args = args[1:] if args and isinstance(args[0], type) else args
arg_str = "_".join(repr(arg) for arg in a_args)
kwarg_str = "_".join(f"{k}={repr(v)}" for k, v in sorted(kwargs.items()))
key = f"{arg_str}_{kwarg_str}"
key = "".join(c if c.isalnum() or (c in "-_.") else "_" for c in key)
key = key.strip("_")
file_path = os.path.join(cache_dir, f"{key}.json")

# Try loading from cache
try:
with open(file_path, 'r') as f:
return json.load(f)
except FileNotFoundError:
pass

# Call the actual function
result = func(*args, **kwargs)

# Save result to cache
try:
with open(file_path, 'w') as f:
json.dump(result, f)
except Exception as e:
print(f"Failed to write cache {file_path}: {e}")

return result

return wrapper


class BibliographicalEntryNotFound(Exception):
Expand Down
10 changes: 10 additions & 0 deletions ifbcat_api/model/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ def get_permission_classes(cls):
IsAuthenticatedOrReadOnly,
)

@classmethod
def get_edam_info_ebi_ols_url(cls, uri):
return f'https://www.ebi.ac.uk/ols4/api/ontologies/edam/terms?iri={uri}'

@classmethod
@misc.disk_cache
def get_edam_from_ebi_ols(cls, uri):
return requests.get(cls.get_edam_info_ebi_ols_url(uri)).json()

def update_information_from_ebi_ols(self):
response = misc.get_edam_info_from_ols(self.uri)
try:
Expand Down Expand Up @@ -306,6 +315,7 @@ def get_permission_classes(cls):
)

@classmethod
@misc.disk_cache
def get_doi_from_pmid(cls, pmid):
with Entrez.efetch(db="pubmed", id=str(pmid), rettype="xml", retmode="text") as handle:
d = Entrez.read(handle)
Expand Down
3 changes: 3 additions & 0 deletions ifbcat_api/static/css/ifbcat_admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -285,4 +285,7 @@ html, body{
#jazzy-tabs{
margin-bottom: 0!important;
}
.dev-instance .content-wrapper{
background-color: orange !important;
}
/**/
4 changes: 3 additions & 1 deletion ifbcat_api/tests/test_no_views_crash.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def test_dashboard(self):
self.assertEqual(self.client.get('/admin/').status_code, 200)


class TestNoViewsCrash(EnsureImportDataAreHere):
class TestCaseWithData(EnsureImportDataAreHere):
def setUp(self):
super().setUp()
# load the whole catalogue
Expand All @@ -79,6 +79,8 @@ def setUp(self):
add_everywhere(f)
logger.warning('Data loaded')


class TestNoViewsCrash(TestCaseWithData):
def test_all_at_once_to_spare_resource(self):
#######################################################################
# test_loadcatalog
Expand Down
45 changes: 45 additions & 0 deletions ifbcat_api/tests/test_sitemap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import re
from collections import defaultdict

from django.urls import reverse

from ifbcat_api import models
from ifbcat_api.tests.test_no_views_crash import TestCaseWithData


class TestApi(TestCaseWithData):
by_passed_links = {
'http://testserver/api/team/Pasteur%20HUB/',
}
link_count_to_test = 10

def test_create_new_file(self):
response = self.client.get(reverse('sitemap-general'))
resp = response.content.decode()
url_pattern = r'<loc>(.*?)</loc>'
links = re.findall(url_pattern, resp)
grouped_links = defaultdict(set)

for link in links:
group = link.find('/', link.find('api') + 4)
grouped_links[group].add(link)

total = 0
for links in grouped_links.values():
links_to_test = list(links)[: self.link_count_to_test]
links_to_test.extend(self.by_passed_links & links)

for link in links_to_test:
self.assertEqual(self.client.get(link, follow=True).status_code, 200, link)
total += 1
self.assertGreater(total, 0)


class TestApiCustomInstance(TestApi):
link_count_to_test = 1000

def setUp(self):
# don't call super().setUp(), create instance instead, can only be an instance in the sitemap
models.Team.objects.get_or_create(
name="foo",
)
2 changes: 1 addition & 1 deletion ifbcat_api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
# path('testapiview/', views.TestApiView.as_view()),
path('tool/<int:pk>/', views.ToolViewSet.as_view({'get': 'retrieve'})),
path('tool/<biotoolsID>/', views.ToolViewSet.as_view({'get': 'retrieve'})),
re_path('topic/(?P<uri>topic_\d+)/', views.TopicViewSet.as_view({'get': 'retrieve'})),
re_path(r'topic/(?P<uri>topic_\d+)/', views.TopicViewSet.as_view({'get': 'retrieve'})),
path('login/', views.UserLoginApiView.as_view()),
path('', include(router.urls)),
path(
Expand Down
Loading