Compare commits

..

No commits in common. "02e6c7c16c980d1231b2b851f026ed398416ac16" and "81c6f32a354a1dee9be5f4933d0f6b69951c8776" have entirely different histories.

17 changed files with 351 additions and 239 deletions

2
.gitignore vendored
View File

@ -2,6 +2,8 @@
app/static/gen/ app/static/gen/
volumes/ volumes/
docker-compose.override.yml docker-compose.override.yml
logs/
!logs/dummy
*.env *.env
*.pjentsch-testing *.pjentsch-testing

View File

@ -46,6 +46,7 @@ COPY --chown=nopaque:nopaque app app
COPY --chown=nopaque:nopaque migrations migrations COPY --chown=nopaque:nopaque migrations migrations
COPY --chown=nopaque:nopaque tests tests COPY --chown=nopaque:nopaque tests tests
COPY --chown=nopaque:nopaque boot.sh config.py wsgi.py ./ COPY --chown=nopaque:nopaque boot.sh config.py wsgi.py ./
RUN mkdir logs
EXPOSE 5000 EXPOSE 5000

View File

@ -35,7 +35,7 @@ username@hostname:~$ sudo mount --types cifs --options gid=${USER},password=nopa
# Clone the nopaque repository # Clone the nopaque repository
username@hostname:~$ git clone https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git username@hostname:~$ git clone https://gitlab.ub.uni-bielefeld.de/sfb1288inf/nopaque.git
# Create data directories # Create data directories
username@hostname:~$ mkdir volumes/{db,mq} username@hostname:~$ mkdir data/{db,logs,mq}
username@hostname:~$ cp db.env.tpl db.env username@hostname:~$ cp db.env.tpl db.env
username@hostname:~$ cp .env.tpl .env username@hostname:~$ cp .env.tpl .env
# Fill out the variables within these files. # Fill out the variables within these files.

View File

@ -14,11 +14,10 @@ from flask_sqlalchemy import SQLAlchemy
from flask_hashids import Hashids from flask_hashids import Hashids
docker_client = DockerClient.from_env()
apifairy = APIFairy() apifairy = APIFairy()
assets = Environment() assets = Environment()
db = SQLAlchemy() db = SQLAlchemy()
docker_client = DockerClient()
hashids = Hashids() hashids = Hashids()
login = LoginManager() login = LoginManager()
ma = Marshmallow() ma = Marshmallow()
@ -29,129 +28,80 @@ scheduler = APScheduler()
socketio = SocketIO() socketio = SocketIO()
def create_app(config: Config = Config) -> Flask: # TODO: Create export for lemmatized corpora
''' Creates an initialized Flask object. '''
def create_app(config: Config = Config) -> Flask:
''' Creates an initialized Flask (WSGI Application) object. '''
app = Flask(__name__) app = Flask(__name__)
app.config.from_object(config) app.config.from_object(config)
config.init_app(app)
_configure_logging(app)
_configure_middlewares(app)
_init_docker_client(app)
_init_extensions(app)
_register_blueprints(app)
_register_socketio_namespaces(app)
_register_db_event_listeners(app)
return app
def _configure_logging(app: Flask):
from flask.logging import default_handler
from logging import Formatter, StreamHandler
log_date_format: str = app.config['NOPAQUE_LOG_DATE_FORMAT']
log_format: str = app.config['NOPAQUE_LOG_FORMAT']
log_level: str = app.config['NOPAQUE_LOG_LEVEL']
formatter = Formatter(fmt=log_format, datefmt=log_date_format)
handler = StreamHandler()
handler.setFormatter(formatter)
handler.setLevel(log_level)
app.logger.removeHandler(default_handler)
app.logger.addHandler(handler)
def _configure_middlewares(app: Flask):
from werkzeug.middleware.proxy_fix import ProxyFix
if app.config['NOPAQUE_PROXY_FIX_ENABLED']:
app.wsgi_app = ProxyFix(
app.wsgi_app,
x_for=app.config['NOPAQUE_PROXY_FIX_X_FOR'],
x_host=app.config['NOPAQUE_PROXY_FIX_X_HOST'],
x_port=app.config['NOPAQUE_PROXY_FIX_X_PORT'],
x_prefix=app.config['NOPAQUE_PROXY_FIX_X_PREFIX'],
x_proto=app.config['NOPAQUE_PROXY_FIX_X_PROTO']
)
def _init_docker_client(app: Flask):
registry: str = app.config['NOPAQUE_DOCKER_REGISTRY']
username: str = app.config['NOPAQUE_DOCKER_REGISTRY_USERNAME']
password: str = app.config['NOPAQUE_DOCKER_REGISTRY_PASSWORD']
docker_client.login( docker_client.login(
username, app.config['NOPAQUE_DOCKER_REGISTRY_USERNAME'],
password=password, password=app.config['NOPAQUE_DOCKER_REGISTRY_PASSWORD'],
registry=registry registry=app.config['NOPAQUE_DOCKER_REGISTRY']
) )
def _init_extensions(app: Flask):
from typing import Callable
from .daemon import daemon
from .models import AnonymousUser, User
is_primary_instance: bool = app.config['NOPAQUE_IS_PRIMARY_INSTANCE']
socketio_message_queue_uri: str = app.config['NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI']
login_user_loader_callback: Callable[[int], User | None] = lambda user_id: User.query.get(int(user_id))
apifairy.init_app(app) apifairy.init_app(app)
assets.init_app(app) assets.init_app(app)
db.init_app(app) db.init_app(app)
hashids.init_app(app) hashids.init_app(app)
login.init_app(app) login.init_app(app)
login.anonymous_user = AnonymousUser
login.login_view = 'auth.login'
login.user_loader(login_user_loader_callback)
ma.init_app(app) ma.init_app(app)
mail.init_app(app) mail.init_app(app)
migrate.init_app(app, db) migrate.init_app(app, db)
paranoid.init_app(app) paranoid.init_app(app)
paranoid.redirect_view = '/'
scheduler.init_app(app) scheduler.init_app(app)
if is_primary_instance: socketio.init_app(app, message_queue=app.config['NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI']) # noqa
scheduler.add_job('daemon', daemon, args=(app,), seconds=3, trigger='interval')
socketio.init_app(app, message_queue=socketio_message_queue_uri)
from .models import AnonymousUser, User
login.anonymous_user = AnonymousUser
login.login_view = 'auth.login'
@login.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
paranoid.redirect_view = '/'
from .models.event_listeners import register_event_listeners
register_event_listeners()
def _register_blueprints(app: Flask):
from .admin import bp as admin_blueprint from .admin import bp as admin_blueprint
from .api import bp as api_blueprint
from .auth import bp as auth_blueprint
from .contributions import bp as contributions_blueprint
from .corpora import bp as corpora_blueprint
from .errors import bp as errors_bp
from .jobs import bp as jobs_blueprint
from .main import bp as main_blueprint
from .services import bp as services_blueprint
from .settings import bp as settings_blueprint
from .users import bp as users_blueprint
from .workshops import bp as workshops_blueprint
app.register_blueprint(admin_blueprint, url_prefix='/admin') app.register_blueprint(admin_blueprint, url_prefix='/admin')
from .api import bp as api_blueprint
app.register_blueprint(api_blueprint, url_prefix='/api') app.register_blueprint(api_blueprint, url_prefix='/api')
from .auth import bp as auth_blueprint
app.register_blueprint(auth_blueprint) app.register_blueprint(auth_blueprint)
from .contributions import bp as contributions_blueprint
app.register_blueprint(contributions_blueprint, url_prefix='/contributions') app.register_blueprint(contributions_blueprint, url_prefix='/contributions')
from .corpora import bp as corpora_blueprint
from .corpora.cqi_over_sio import CQiNamespace
app.register_blueprint(corpora_blueprint, cli_group='corpus', url_prefix='/corpora') app.register_blueprint(corpora_blueprint, cli_group='corpus', url_prefix='/corpora')
socketio.on_namespace(CQiNamespace('/cqi_over_sio'))
from .errors import bp as errors_bp
app.register_blueprint(errors_bp) app.register_blueprint(errors_bp)
from .jobs import bp as jobs_blueprint
app.register_blueprint(jobs_blueprint, url_prefix='/jobs') app.register_blueprint(jobs_blueprint, url_prefix='/jobs')
from .main import bp as main_blueprint
app.register_blueprint(main_blueprint, cli_group=None) app.register_blueprint(main_blueprint, cli_group=None)
from .services import bp as services_blueprint
app.register_blueprint(services_blueprint, url_prefix='/services') app.register_blueprint(services_blueprint, url_prefix='/services')
from .settings import bp as settings_blueprint
app.register_blueprint(settings_blueprint, url_prefix='/settings') app.register_blueprint(settings_blueprint, url_prefix='/settings')
from .users import bp as users_blueprint
app.register_blueprint(users_blueprint, cli_group='user', url_prefix='/users') app.register_blueprint(users_blueprint, cli_group='user', url_prefix='/users')
from .workshops import bp as workshops_blueprint
app.register_blueprint(workshops_blueprint, url_prefix='/workshops') app.register_blueprint(workshops_blueprint, url_prefix='/workshops')
return app
def _register_socketio_namespaces(app: Flask):
from .corpora.cqi_over_sio import CQiOverSocketIO
socketio.on_namespace(CQiOverSocketIO('/cqi_over_sio'))
def _register_db_event_listeners(app: Flask):
from .models.event_listeners import register_event_listeners
register_event_listeners()

View File

@ -19,7 +19,7 @@ This package tunnels the Corpus Query interface (CQi) protocol through
Socket.IO (SIO) by tunneling CQi API calls through an event called "exec". Socket.IO (SIO) by tunneling CQi API calls through an event called "exec".
Basic concept: Basic concept:
1. A client connects to the namespace. 1. A client connects to the "/cqi_over_sio" namespace.
2. The client emits the "init" event and provides a corpus id for the corpus 2. The client emits the "init" event and provides a corpus id for the corpus
that should be analysed in this session. that should be analysed in this session.
1.1 The analysis session counter of the corpus is incremented. 1.1 The analysis session counter of the corpus is incremented.
@ -28,13 +28,14 @@ Basic concept:
1.4 Connect the CQiClient to the server. 1.4 Connect the CQiClient to the server.
1.5 Save the CQiClient, the Lock and the corpus id in the session for 1.5 Save the CQiClient, the Lock and the corpus id in the session for
subsequential use. subsequential use.
3. The client emits "exec" events, within which it provides the name of a CQi 2. The client emits the "exec" event provides the name of a CQi API function
API function and the corresponding arguments. arguments (optional).
3.1 The "exec" event handler will execute the function, make sure that - The event "exec" handler will execute the function, make sure that the
the result is serializable and returns the result back to the client. result is serializable and returns the result back to the client.
4. The client disconnects from the namespace 4. Wait for more events
4.1 The analysis session counter of the corpus is decremented. 5. The client disconnects from the "/cqi_over_sio" namespace
4.2 The CQiClient and (Mutex) Lock belonging to it are teared down. 1.1 The analysis session counter of the corpus is decremented.
1.2 The CQiClient and (Mutex) Lock belonging to it are teared down.
''' '''
CQI_API_FUNCTION_NAMES: List[str] = [ CQI_API_FUNCTION_NAMES: List[str] = [
@ -85,7 +86,7 @@ CQI_API_FUNCTION_NAMES: List[str] = [
] ]
class CQiOverSocketIO(Namespace): class CQiNamespace(Namespace):
@socketio_login_required @socketio_login_required
def on_connect(self): def on_connect(self):
pass pass

View File

@ -14,7 +14,7 @@ from .forms import CreateCorpusForm
@bp.route('') @bp.route('')
def corpora(): def corpora():
return redirect(url_for('main.dashboard', _anchor='corpora')) return render_template('corpora/corpora.html.j2', title='Corpora')
@bp.route('/create', methods=['GET', 'POST']) @bp.route('/create', methods=['GET', 'POST'])

View File

@ -1,5 +1,5 @@
from flask import Flask
from app import db from app import db
from flask import Flask
from .corpus_utils import check_corpora from .corpus_utils import check_corpora
from .job_utils import check_jobs from .job_utils import check_jobs

View File

@ -12,7 +12,8 @@ from . import bp
@bp.route('') @bp.route('')
def jobs(): def jobs():
return redirect(url_for('main.dashboard', _anchor='jobs')) # return redirect(url_for('main.dashboard', _anchor='jobs'))
return render_template('jobs/jobs.html.j2', title='Jobs')
@bp.route('/<hashid:job_id>') @bp.route('/<hashid:job_id>')

View File

@ -12,7 +12,7 @@ body[data-sidenav-fixed="true" i] main,
body[data-sidenav-fixed="true" i] footer { body[data-sidenav-fixed="true" i] footer {
padding-left: 300px; padding-left: 300px;
} }
@media only screen and (max-width : 992px) { @media only screen and (max-width : 992px) {
body[data-sidenav-fixed="true" i] header, body[data-sidenav-fixed="true" i] header,
body[data-sidenav-fixed="true" i] main, body[data-sidenav-fixed="true" i] main,
@ -34,7 +34,7 @@ body[data-sidenav-fixed="true" i] .navbar-fixed > nav {
/* #region sticky-footer */ /* #region sticky-footer */
/* /*
* Sticky Footer: * Sticky Footer:
* A sticky footer always stays on the bottom of the page regardless of how * A sticky footer always stays on the bottom of the page regardless of how
* little content is on the page. However, this footer will be pushed down if * little content is on the page. However, this footer will be pushed down if
* there is a lot of content, so it is different from a fixed footer. * there is a lot of content, so it is different from a fixed footer.

View File

@ -35,67 +35,3 @@
/* Support for IE. */ /* Support for IE. */
font-feature-settings: 'liga'; font-feature-settings: 'liga';
} }
[data-nopaque-icon="speedometer"] {
--nopaque-icon-character: "A";
--nopaque-negative-icon-character: "a";
}
[data-nopaque-icon="cloud-processes"] {
--nopaque-icon-character: "B";
--nopaque-negative-icon-character: "b";
}
[data-nopaque-icon="account-circle"] {
--nopaque-icon-character: "C";
--nopaque-negative-icon-character: "c";
}
[data-nopaque-icon="meshed-gears"] {
--nopaque-icon-character: "D";
--nopaque-negative-icon-character: "d";
}
[data-nopaque-icon="file-setup-pipeline"] {
--nopaque-icon-character: "E";
--nopaque-negative-icon-character: "e";
}
[data-nopaque-icon="tesseract-ocr-pipeline"],
[data-nopaque-icon="transkribus-htr-pipeline"] {
--nopaque-icon-character: "F";
--nopaque-negative-icon-character: "f";
}
[data-nopaque-icon="spacy-nlp-pipeline"] {
--nopaque-icon-character: "G";
--nopaque-negative-icon-character: "g";
}
[data-nopaque-icon="corpus-analysis"] {
--nopaque-icon-character: "H";
--nopaque-negative-icon-character: "h";
}
[data-nopaque-icon="file-cabinet"] {
--nopaque-icon-character: "I";
--nopaque-negative-icon-character: "i";
}
[data-nopaque-icon="suitcase"] {
--nopaque-icon-character: "J";
--nopaque-negative-icon-character: "j";
}
[data-nopaque-icon="wrench"] {
--nopaque-icon-character: "K";
--nopaque-negative-icon-character: "k";
}
.nopaque-icons[data-nopaque-icon]:empty::before {
content: var(--nopaque-icon-character);
}
.nopaque-icons.negative[data-nopaque-icon]:empty::before{
content: var(--nopaque-negative-icon-character);
}

View File

@ -33,7 +33,7 @@
<a class="waves-effect" href="{{ url_for('jobs.jobs') }}"><i class="nopaque-icons">J</i>My Jobs</a> <a class="waves-effect" href="{{ url_for('jobs.jobs') }}"><i class="nopaque-icons">J</i>My Jobs</a>
</li> </li>
<li> <li>
<a class="waves-effect" href="{{ url_for('contributions.contributions') }}"><i class="material-icons">new_label</i>My Contributions</a> <a class="waves-effect" href="{{ url_for('main.dashboard', _anchor='contributions') }}"><i class="material-icons">new_label</i>My Contributions</a>
</li> </li>
{# processes & services items #} {# processes & services items #}

View File

@ -0,0 +1,38 @@
{% extends "base.html.j2" %}
{% block page_content %}
<div class="container">
<h1>{{ title }}</h1>
<div class="card">
<div class="card-content">
<div class="corpus-list" data-user-id="{{ current_user.hashid }}"></div>
</div>
<div class="card-action right-align">
<a class="btn waves-effect waves-light" href="{{ url_for('corpora.create_corpus') }}">Create corpus<i class="material-icons right">add</i></a>
</div>
</div>
</div>
{#
<div class="parallax-container">
<div class="parallax">
<img src="{{ url_for('static', filename='images/parallax_lq/01_books_antique_book_old.jpg') }}">
</div>
</div>
<div class="container">
<div style="position: relative; top: -225px;">
<div class="card white">
<div class="card-content">
<div class="corpus-list" data-user-id="{{ current_user.hashid }}"></div>
</div>
<div class="card-action right-align">
<a class="btn waves-effect waves-light" href="{{ url_for('corpora.create_corpus') }}">Create corpus<i class="material-icons right">add</i></a>
</div>
</div>
</div>
</div>
#}
{% endblock page_content %}

View File

@ -0,0 +1,98 @@
{% extends "base.html.j2" %}
{% block page_content %}
<div class="container">
<h1>{{ title }}</h1>
<div class="card">
<div class="card-content">
<div class="job-list" data-user-id="{{ current_user.hashid }}"></div>
</div>
<div class="card-action right-align">
<a class="btn modal-trigger waves-effect waves-light" data-target="create-job-modal">Create job<i class="material-icons right">add</i></a>
</div>
</div>
<div class="fixed-action-btn">
<a class="btn-floating btn-large">
<i class="large material-icons">add</i>
</a>
<ul>
<li>
<a class="btn-floating service-color darken tooltipped" data-position="left" data-tooltip="File Setup Pipeline" data-service="file-setup-pipeline">
<i class="nopaque-icons service-icons" data-service="file-setup-pipeline"></i>
</a>
</li>
<li>
<a class="btn-floating service-color darken tooltipped" data-position="left" data-tooltip="Tesseract OCR Pipeline" data-service="tesseract-ocr-pipeline">
<i class="nopaque-icons service-icons" data-service="tesseract-ocr-pipeline"></i>
</a>
</li>
{% if config.NOPAQUE_TRANSKRIBUS_ENABLED %}
<li>
<a class="btn-floating service-color darken tooltipped" data-position="left" data-tooltip="Transkribus HTR Pipeline" data-service="transkribus-htr-pipeline">
<i class="nopaque-icons service-icons" data-service="transkribus-htr-pipeline"></i>
</a>
</li>
{% endif %}
<li>
<a class="btn-floating service-color darken tooltipped" data-position="left" data-tooltip="SpaCy NLP Pipeline" data-service="spacy-nlp-pipeline">
<i class="nopaque-icons service-icons" data-service="spacy-nlp-pipeline"></i>
</a>
</li>
</ul>
</div>
</div>
{#
<div class="parallax-container">
<div class="parallax">
<img src="{{ url_for('static', filename='images/parallax_lq/01_books_antique_book_old.jpg') }}">
</div>
</div>
<div class="container">
<div class="fixed-action-btn">
<a class="btn-floating btn-large">
<i class="large material-icons">add</i>
</a>
<ul>
<li>
<a class="btn-floating service-color darken tooltipped" data-position="left" data-tooltip="File Setup Pipeline" data-service="file-setup-pipeline">
<i class="nopaque-icons service-icons" data-service="file-setup-pipeline"></i>
</a>
</li>
<li>
<a class="btn-floating service-color darken tooltipped" data-position="left" data-tooltip="Tesseract OCR Pipeline" data-service="tesseract-ocr-pipeline">
<i class="nopaque-icons service-icons" data-service="tesseract-ocr-pipeline"></i>
</a>
</li>
{% if config.NOPAQUE_TRANSKRIBUS_ENABLED %}
<li>
<a class="btn-floating service-color darken tooltipped" data-position="left" data-tooltip="Transkribus HTR Pipeline" data-service="transkribus-htr-pipeline">
<i class="nopaque-icons service-icons" data-service="transkribus-htr-pipeline"></i>
</a>
</li>
{% endif %}
<li>
<a class="btn-floating service-color darken tooltipped" data-position="left" data-tooltip="SpaCy NLP Pipeline" data-service="spacy-nlp-pipeline">
<i class="nopaque-icons service-icons" data-service="spacy-nlp-pipeline"></i>
</a>
</li>
</ul>
</div>
<div style="position: relative; top: -90px;">
<div class="card">
<div class="card-content">
<div class="job-list" data-user-id="{{ current_user.hashid }}"></div>
</div>
<div class="card-action right-align">
<a class="btn modal-trigger waves-effect waves-light" data-target="create-job-modal">Create job<i class="material-icons right">add</i></a>
</div>
</div>
</div>
</div>
#}
{% endblock page_content %}

View File

@ -9,6 +9,7 @@
<div class="col s12" id="corpora"> <div class="col s12" id="corpora">
<h2>My Corpora</h2> <h2>My Corpora</h2>
<p>Hallo <span class="pink-text">Audrey</span></p>
<p>Create a corpus to interactively perform linguistic analysis.</p> <p>Create a corpus to interactively perform linguistic analysis.</p>
</div> </div>
@ -33,6 +34,7 @@
pre-processing step. You will need the result of each step for the pre-processing step. You will need the result of each step for the
next step. next step.
</p> </p>
<p><b>Where is my Job data?</b> Don't worry, please read <a href="{{ url_for('main.news', _anchor='april-2022-update') }}">this news</a> entry</p>
</div> </div>
<div class="col s12"> <div class="col s12">
@ -54,7 +56,7 @@
<div class="card extension-selector hoverable service-color" data-service="tesseract-ocr-pipeline"> <div class="card extension-selector hoverable service-color" data-service="tesseract-ocr-pipeline">
<a href="{{ url_for('contributions.tesseract_ocr_pipeline_models') }}" style="position: absolute; width: 100%; height: 100%;"></a> <a href="{{ url_for('contributions.tesseract_ocr_pipeline_models') }}" style="position: absolute; width: 100%; height: 100%;"></a>
<div class="card-content"> <div class="card-content">
<span class="card-title">Tesseract OCR Pipeline Models</span> <span class="card-title" data-service="tesseract-ocr-pipeline">Tesseract OCR Pipeline Models</span>
<p>Here you can see and edit the models that you have created. You can also create new models.</p> <p>Here you can see and edit the models that you have created. You can also create new models.</p>
</div> </div>
</div> </div>

View File

@ -20,22 +20,22 @@
<div class="col s12"> <div class="col s12">
<div class="row"> <div class="row">
<div class="col s12 m6 l3 center-align"> <div class="col s12 m6 l3 center-align">
<i class="large nopaque-icons primary-color-text" data-nopaque-icon="speedometer"></i><br> <i class="large nopaque-icons primary-color-text">A</i><br>
<b class="primary-color-text">Speeds up your work</b> <b class="primary-color-text">Speeds up your work</b>
<p class="light">All tools provided by nopaque are carefully selected to provide a complete tool suite without being held up by compatibility issues.</p> <p class="light">All tools provided by nopaque are carefully selected to provide a complete tool suite without being held up by compatibility issues.</p>
</div> </div>
<div class="col s12 m6 l3 center-align"> <div class="col s12 m6 l3 center-align">
<i class="large nopaque-icons primary-color-text" data-nopaque-icon="cloud-processes"></i><br> <i class="large nopaque-icons primary-color-text">B</i><br>
<b class="primary-color-text">Cloud infrastructure</b> <b class="primary-color-text">Cloud infrastructure</b>
<p class="light">All computational work is processed within nopaques cloud infrastructure. You don't need to install any software. Great, right?</p> <p class="light">All computational work is processed within nopaques cloud infrastructure. You don't need to install any software. Great, right?</p>
</div> </div>
<div class="col s12 m6 l3 center-align"> <div class="col s12 m6 l3 center-align">
<i class="large nopaque-icons primary-color-text" data-nopaque-icon="account-circle"></i><br> <i class="large nopaque-icons primary-color-text">C</i><br>
<b class="primary-color-text">User friendly</b> <b class="primary-color-text">User friendly</b>
<p class="light">You can start right away without having to read mile-long manuals. All services come with default settings that make it easy for you to just get going. Also great, right?</p> <p class="light">You can start right away without having to read mile-long manuals. All services come with default settings that make it easy for you to just get going. Also great, right?</p>
</div> </div>
<div class="col s12 m6 l3 center-align"> <div class="col s12 m6 l3 center-align">
<i class="large nopaque-icons primary-color-text" data-nopaque-icon="meshed-gears"></i><br> <i class="large nopaque-icons primary-color-text">D</i><br>
<b class="primary-color-text">Meshing processes</b> <b class="primary-color-text">Meshing processes</b>
<p class="light">No matter where you step in, nopaque facilitates and accompanies your research. Its workflow perfectly ties in with your research process.</p> <p class="light">No matter where you step in, nopaque facilitates and accompanies your research. Its workflow perfectly ties in with your research process.</p>
</div> </div>

179
config.py
View File

@ -1,54 +1,47 @@
from dotenv import load_dotenv from dotenv import load_dotenv
from flask import Flask
from logging.handlers import RotatingFileHandler
from pathlib import Path from pathlib import Path
from werkzeug.middleware.proxy_fix import ProxyFix
import logging
import os import os
BASE_DIR = os.path.abspath(os.path.dirname(__file__)) basedir = os.path.abspath(os.path.dirname(__file__))
ENV_FILE = os.path.join(BASE_DIR, 'nopaque.env') load_dotenv(os.path.join(basedir, 'nopaque.env'))
if os.path.isfile(ENV_FILE):
load_dotenv(ENV_FILE)
class Config: class Config:
''' Configuration class for the Flask application. ''' ''' APIFairy '''
# region APIFairy
APIFAIRY_TITLE = 'nopaque' APIFAIRY_TITLE = 'nopaque'
APIFAIRY_VERSION = '0.0.1' APIFAIRY_VERSION = '0.0.1'
APIFAIRY_APISPEC_PATH = '/api/apispec.json'
APIFAIRY_UI = 'swagger_ui' APIFAIRY_UI = 'swagger_ui'
APIFAIRY_UI_PATH = '/api/docs' APIFAIRY_APISPEC_PATH = '/api/apispec.json'
# endregion APIFairy APIFAIRY_UI_PATH = '/api'
''' # Flask # '''
# region Flask APPLICATION_ROOT = os.environ.get('APPLICATION_ROOT', '/')
DEBUG = os.environ.get('FLASK_DEBUG', 'false').lower() == 'true'
PREFERRED_URL_SCHEME = os.environ.get('PREFERRED_URL_SCHEME', 'http') PREFERRED_URL_SCHEME = os.environ.get('PREFERRED_URL_SCHEME', 'http')
SECRET_KEY = os.environ.get('SECRET_KEY', 'hard to guess string') SECRET_KEY = os.environ.get('SECRET_KEY', 'hard to guess string')
SERVER_NAME = os.environ.get('SERVER_NAME', 'localhost:5000') SERVER_NAME = os.environ.get('SERVER_NAME', 'localhost:5000')
SESSION_COOKIE_SECURE = os.environ.get('SESSION_COOKIE_SECURE', 'false').lower() == 'true' SESSION_COOKIE_SECURE = \
# endregion Flask os.environ.get('SESSION_COOKIE_SECURE', 'false').lower() == 'true'
''' # Flask-APScheduler # '''
JOBS = []
# region Flask-Assets ''' # Flask-Assets '''
ASSETS_DEBUG = os.environ.get('ASSETS_DEBUG', 'false').lower() == 'true' ASSETS_DEBUG = os.environ.get('ASSETS_DEBUG', 'false').lower() == 'true'
# endregion Flask-Assets
''' # Flask-Hashids '''
# region Flask-Hashids
HASHIDS_MIN_LENGTH = int(os.environ.get('HASHIDS_MIN_LENGTH', '16')) HASHIDS_MIN_LENGTH = int(os.environ.get('HASHIDS_MIN_LENGTH', '16'))
HASHIDS_SALT = os.environ.get('HASHIDS_SALT', 'hard to guess string') HASHIDS_SALT = os.environ.get('HASHIDS_SALT', 'hard to guess string')
# endregion Flask-Hashids
''' # Flask-Login # '''
REMEMBER_COOKIE_SECURE = \
os.environ.get('REMEMBER_COOKIE_SECURE', 'false').lower() == 'true'
# region Flask-Login ''' # Flask-Mail # '''
REMEMBER_COOKIE_SECURE = os.environ.get('REMEMBER_COOKIE_SECURE', 'false').lower() == 'true'
# endregion Flask-Login
# region Flask-Mail
MAIL_DEFAULT_SENDER = os.environ.get('MAIL_DEFAULT_SENDER') MAIL_DEFAULT_SENDER = os.environ.get('MAIL_DEFAULT_SENDER')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
MAIL_SERVER = os.environ.get('MAIL_SERVER') MAIL_SERVER = os.environ.get('MAIL_SERVER')
@ -56,53 +49,125 @@ class Config:
MAIL_USERNAME = os.environ.get('MAIL_USERNAME') MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_USE_SSL = os.environ.get('MAIL_USE_SSL', 'false').lower() == 'true' MAIL_USE_SSL = os.environ.get('MAIL_USE_SSL', 'false').lower() == 'true'
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'false').lower() == 'true' MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'false').lower() == 'true'
# endregion Flask-Mail
''' # Flask-SQLAlchemy # '''
# region Flask-SQLAlchemy SQLALCHEMY_DATABASE_URI = \
SQLALCHEMY_DATABASE_URI = os.environ.get( os.environ.get('SQLALCHEMY_DATABASE_URI') \
'SQLALCHEMY_DATABASE_URI', or f'sqlite:///{os.path.join(basedir, "data.sqlite")}'
f'sqlite:///{BASE_DIR}/data.sqlite'
)
SQLALCHEMY_RECORD_QUERIES = True SQLALCHEMY_RECORD_QUERIES = True
SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_TRACK_MODIFICATIONS = False
# endregion Flask-SQLAlchemy
''' # nopaque # '''
# region nopaque
NOPAQUE_ADMIN = os.environ.get('NOPAQUE_ADMIN') NOPAQUE_ADMIN = os.environ.get('NOPAQUE_ADMIN')
NOPAQUE_DATA_DIR = Path(os.environ.get('NOPAQUE_DATA_PATH', '/mnt/nopaque')) NOPAQUE_DATA_DIR = Path(os.environ.get('NOPAQUE_DATA_PATH', '/mnt/nopaque'))
NOPAQUE_IS_PRIMARY_INSTANCE = os.environ.get('NOPAQUE_IS_PRIMARY_INSTANCE', 'true').lower() == 'true' NOPAQUE_IS_PRIMARY_INSTANCE = \
os.environ.get('NOPAQUE_IS_PRIMARY_INSTANCE', 'true').lower() == 'true'
NOPAQUE_MAIL_SUBJECT_PREFIX = '[nopaque]' NOPAQUE_MAIL_SUBJECT_PREFIX = '[nopaque]'
NOPAQUE_SERVICE_DESK = 'gitlab-ub-incoming+sfb1288inf-nopaque-1324-issue-@jura.uni-bielefeld.de' # noqa NOPAQUE_SERVICE_DESK = 'gitlab-ub-incoming+sfb1288inf-nopaque-1324-issue-@jura.uni-bielefeld.de' # noqa
NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI = os.environ.get('NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI') NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI = \
os.environ.get('NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI')
NOPAQUE_JOB_EXPIRATION_ENABLED = os.environ.get('NOPAQUE_JOB_EXPIRATION_ENABLED', 'true').lower() == 'true'
NOPAQUE_JOB_EXPIRATION_TIME = int(os.environ.get('NOPAQUE_JOB_EXPIRATION_TIME', '120'))
NOPAQUE_DOCKER_REGISTRY = 'gitlab.ub.uni-bielefeld.de:4567' NOPAQUE_DOCKER_REGISTRY = 'gitlab.ub.uni-bielefeld.de:4567'
NOPAQUE_DOCKER_IMAGE_PREFIX = f'{NOPAQUE_DOCKER_REGISTRY}/sfb1288inf/' NOPAQUE_DOCKER_IMAGE_PREFIX = f'{NOPAQUE_DOCKER_REGISTRY}/sfb1288inf/'
NOPAQUE_DOCKER_NETWORK_NAME = os.environ.get('DOCKER_NETWORK_NAME', 'nopaque') NOPAQUE_DOCKER_NETWORK_NAME = \
NOPAQUE_DOCKER_REGISTRY_USERNAME = os.environ.get('NOPAQUE_DOCKER_REGISTRY_USERNAME') os.environ.get('DOCKER_NETWORK_NAME', 'nopaque')
NOPAQUE_DOCKER_REGISTRY_PASSWORD = os.environ.get('NOPAQUE_DOCKER_REGISTRY_PASSWORD') NOPAQUE_DOCKER_REGISTRY_USERNAME = \
os.environ.get('NOPAQUE_DOCKER_REGISTRY_USERNAME')
NOPAQUE_DOCKER_REGISTRY_PASSWORD = \
os.environ.get('NOPAQUE_DOCKER_REGISTRY_PASSWORD')
NOPAQUE_LOG_DATE_FORMAT = os.environ.get( NOPAQUE_LOG_DATE_FORMAT = \
'NOPAQUE_LOG_DATE_FORMAT', os.environ.get('NOPAQUE_LOG_DATE_FORMAT', '%Y-%m-%d %H:%M:%S')
'%Y-%m-%d %H:%M:%S'
)
NOPAQUE_LOG_FORMAT = os.environ.get( NOPAQUE_LOG_FORMAT = os.environ.get(
'NOPAQUE_LOG_DATE_FORMAT', 'NOPAQUE_LOG_DATE_FORMAT',
'[%(asctime)s] %(levelname)s in %(pathname)s (function: %(funcName)s, line: %(lineno)d): %(message)s' '[%(asctime)s] %(levelname)s in '
'%(pathname)s (function: %(funcName)s, line: %(lineno)d): %(message)s'
) )
NOPAQUE_LOG_LEVEL = os.environ.get('NOPAQUE_LOG_LEVEL', 'WARNING') NOPAQUE_LOG_FILE_ENABLED = \
os.environ.get('NOPAQUE_LOG_FILE_ENABLED', 'false').lower() == 'true'
NOPAQUE_LOG_FILE_DIR = Path(os.environ.get('NOPAQUE_LOG_FILE_DIR', '/var/log/nopaque'))
NOPAQUE_LOG_FILE_LEVEL = \
os.environ.get('NOPAQUE_LOG_FILE_LEVEL', None)
NOPAQUE_LOG_STDERR_ENABLED = \
os.environ.get('NOPAQUE_LOG_STDERR_ENABLED', 'true').lower() == 'true'
NOPAQUE_LOG_STDERR_LEVEL = \
os.environ.get('NOPAQUE_LOG_STDERR_LEVEL', None)
NOPAQUE_PROXY_FIX_ENABLED = os.environ.get('NOPAQUE_PROXY_FIX_ENABLED', 'false').lower() == 'true' NOPAQUE_PROXY_FIX_ENABLED = \
NOPAQUE_PROXY_FIX_X_FOR = int(os.environ.get('NOPAQUE_PROXY_FIX_X_FOR', '0')) os.environ.get('NOPAQUE_PROXY_FIX_ENABLED', 'false').lower() == 'true'
NOPAQUE_PROXY_FIX_X_HOST = int(os.environ.get('NOPAQUE_PROXY_FIX_X_HOST', '0')) NOPAQUE_PROXY_FIX_X_FOR = \
NOPAQUE_PROXY_FIX_X_PORT = int(os.environ.get('NOPAQUE_PROXY_FIX_X_PORT', '0')) int(os.environ.get('NOPAQUE_PROXY_FIX_X_FOR', '0'))
NOPAQUE_PROXY_FIX_X_PREFIX = int(os.environ.get('NOPAQUE_PROXY_FIX_X_PREFIX', '0')) NOPAQUE_PROXY_FIX_X_HOST = \
NOPAQUE_PROXY_FIX_X_PROTO = int(os.environ.get('NOPAQUE_PROXY_FIX_X_PROTO', '0')) int(os.environ.get('NOPAQUE_PROXY_FIX_X_HOST', '0'))
NOPAQUE_PROXY_FIX_X_PORT = \
int(os.environ.get('NOPAQUE_PROXY_FIX_X_PORT', '0'))
NOPAQUE_PROXY_FIX_X_PREFIX = \
int(os.environ.get('NOPAQUE_PROXY_FIX_X_PREFIX', '0'))
NOPAQUE_PROXY_FIX_X_PROTO = \
int(os.environ.get('NOPAQUE_PROXY_FIX_X_PROTO', '0'))
NOPAQUE_TRANSKRIBUS_ENABLED = os.environ.get('NOPAQUE_TRANSKRIBUS_ENABLED', 'false').lower() == 'true' NOPAQUE_TRANSKRIBUS_ENABLED = \
os.environ.get('NOPAQUE_TRANSKRIBUS_ENABLED', 'false').lower() == 'true'
NOPAQUE_READCOOP_USERNAME = os.environ.get('NOPAQUE_READCOOP_USERNAME') NOPAQUE_READCOOP_USERNAME = os.environ.get('NOPAQUE_READCOOP_USERNAME')
NOPAQUE_READCOOP_PASSWORD = os.environ.get('NOPAQUE_READCOOP_PASSWORD') NOPAQUE_READCOOP_PASSWORD = os.environ.get('NOPAQUE_READCOOP_PASSWORD')
NOPAQUE_VERSION='1.0.2' NOPAQUE_VERSION='1.0.2'
# endregion nopaque
@staticmethod
def init_app(app: Flask):
for handler in app.logger.handlers:
app.logger.removeHandler(handler)
log_formatter = logging.Formatter(
fmt=app.config['NOPAQUE_LOG_FORMAT'],
datefmt=app.config['NOPAQUE_LOG_DATE_FORMAT']
)
if app.config['NOPAQUE_LOG_STDERR_ENABLED']:
log_stderr_level: str | None = app.config['NOPAQUE_LOG_STDERR_LEVEL']
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(log_formatter)
if log_stderr_level is not None:
stream_handler.setLevel(log_stderr_level)
app.logger.addHandler(stream_handler)
if app.config['NOPAQUE_LOG_FILE_ENABLED']:
log_file_dir: Path = app.config['NOPAQUE_LOG_FILE_DIR']
log_file_level: str | None = app.config['NOPAQUE_LOG_FILE_LEVEL']
if not log_file_dir.exists():
log_file_dir.mkdir()
rotating_file_handler = RotatingFileHandler(
log_file_dir / 'nopaque.log',
maxBytes=10_240,
backupCount=10
)
rotating_file_handler.setFormatter(log_formatter)
if log_file_level is not None:
rotating_file_handler.setLevel(log_file_level)
app.logger.addHandler(rotating_file_handler)
if app.config['NOPAQUE_PROXY_FIX_ENABLED']:
# Set up and apply the ProxyFix middleware according to the
# corresponding (NOPAQUE_PROXY_FIX_*) configurations
app.wsgi_app = ProxyFix(
app.wsgi_app,
x_for=app.config['NOPAQUE_PROXY_FIX_X_FOR'],
x_host=app.config['NOPAQUE_PROXY_FIX_X_HOST'],
x_port=app.config['NOPAQUE_PROXY_FIX_X_PORT'],
x_prefix=app.config['NOPAQUE_PROXY_FIX_X_PREFIX'],
x_proto=app.config['NOPAQUE_PROXY_FIX_X_PROTO']
)
if app.config['NOPAQUE_IS_PRIMARY_INSTANCE']:
app.config['JOBS'].append(
{
"id": "daemon",
"func": "app.daemon:daemon",
"args": (app,),
"trigger": "interval",
"seconds": 3,
}
)

View File

@ -7,6 +7,9 @@
# Flask # # Flask #
# https://flask.palletsprojects.com/en/1.1.x/config/ # # https://flask.palletsprojects.com/en/1.1.x/config/ #
############################################################################## ##############################################################################
# DEFAULT: /
# APPLICATION_ROOT=
# CHOOSE ONE: http, https # CHOOSE ONE: http, https
# DEFAULT: http # DEFAULT: http
# PREFERRED_URL_SCHEME= # PREFERRED_URL_SCHEME=
@ -138,9 +141,24 @@ NOPAQUE_DOCKER_REGISTRY_PASSWORD=
# DEFAULT: [%(asctime)s] %(levelname)s in %(pathname)s (function: %(funcName)s, line: %(lineno)d): %(message)s # DEFAULT: [%(asctime)s] %(levelname)s in %(pathname)s (function: %(funcName)s, line: %(lineno)d): %(message)s
# NOPAQUE_LOG_FORMAT= # NOPAQUE_LOG_FORMAT=
# CHOOSE ONE: False, True
# DEFAULT: False
# NOPAQUE_LOG_FILE_ENABLED=
# DEFAULT: /var/log/nopaque
# NOPAQUE_LOG_FILE_DIR=
# DEFAULT: DEBUG if FLASK_DEBUG == True else WARNING # DEFAULT: DEBUG if FLASK_DEBUG == True else WARNING
# CHOOSE ONE: CRITICAL, ERROR, WARNING, INFO, DEBUG # CHOOSE ONE: CRITICAL, ERROR, WARNING, INFO, DEBUG
# NOPAQUE_LOG_LEVEL= # NOPAQUE_LOG_FILE_LEVEL=
# CHOOSE ONE: False, True
# DEFAULT: True
# NOPAQUE_LOG_STDERR_ENABLED=
# DEFAULT: DEBUG if FLASK_DEBUG == True else WARNING
# CHOOSE ONE: CRITICAL, ERROR, WARNING, INFO, DEBUG
# NOPAQUE_LOG_STDERR_LEVEL=
# CHOOSE ONE: False, True # CHOOSE ONE: False, True
# DEFAULT: False # DEFAULT: False