From 23b86530876623786934f43d63a017a663fcab28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9ni=20Marvaud?= <24732919+lmarvaud@users.noreply.github.com> Date: Mon, 19 Sep 2022 15:14:34 +0200 Subject: [PATCH 1/7] :recycle: upgrade flask - LBB-23 upgrade flask - LBB-23 updated requirements - LBB-23 upgrade flask_debug tools - LBB-23 resolved dependancies conflicts - LBB-23 rm flake forced version --- .../conf/common/overrides/development.py | 7 +- labonneboite/common/models/office.py | 2 +- labonneboite/web/api/views.py | 2 +- labonneboite/web/app.py | 2 +- labonneboite/web/search/views.py | 6 +- requirements.dev.in | 6 +- requirements.dev.txt | 64 +++++++++---------- requirements.in | 7 +- requirements.txt | 33 +++++----- 9 files changed, 69 insertions(+), 60 deletions(-) diff --git a/labonneboite/common/conf/common/overrides/development.py b/labonneboite/common/conf/common/overrides/development.py index c39fdb12f..22d1bb2dd 100644 --- a/labonneboite/common/conf/common/overrides/development.py +++ b/labonneboite/common/conf/common/overrides/development.py @@ -6,7 +6,12 @@ DB_HOST = '127.0.0.1' DB_PORT = 3306 -LOG_FORMAT_USER_ACTIVITY = flask.logging.DEBUG_LOG_FORMAT +LOG_FORMAT_USER_ACTIVITY = ( + '-' * 80 + '\n' + + '%(levelname)s in %(module)s [%(pathname)s:%(lineno)d]:\n' + + '%(message)s\n' + + '-' * 80 +) PEAM_VERIFY_SSL = False diff --git a/labonneboite/common/models/office.py b/labonneboite/common/models/office.py index b3ea9c508..7ae3866a7 100644 --- a/labonneboite/common/models/office.py +++ b/labonneboite/common/models/office.py @@ -7,7 +7,7 @@ from flask import url_for from slugify import slugify from sqlalchemy import PrimaryKeyConstraint, Index -from werkzeug import cached_property +from werkzeug.utils import cached_property from labonneboite_common import encoding as encoding_util from labonneboite.common import hiring_type_util diff --git a/labonneboite/web/api/views.py b/labonneboite/web/api/views.py index 5090a4a24..24c6d7448 100644 --- a/labonneboite/web/api/views.py +++ b/labonneboite/web/api/views.py @@ -20,7 +20,7 @@ from labonneboite.web.api import util as api_util from labonneboite.conf import settings from labonneboite.common.search import HiddenMarketFetcher, AudienceFilter, FILTERS, DISTANCE_FILTER_MAX -from flask.ext.cors import cross_origin +from flask_cors import cross_origin apiBlueprint = Blueprint('api', __name__) diff --git a/labonneboite/web/app.py b/labonneboite/web/app.py index 6baf1ecc6..48d376a81 100644 --- a/labonneboite/web/app.py +++ b/labonneboite/web/app.py @@ -16,7 +16,7 @@ from slugify import slugify from social_core import exceptions as social_exceptions from social_flask_sqlalchemy.models import init_social -from werkzeug.contrib.fixers import ProxyFix +from werkzeug.middleware.proxy_fix import ProxyFix from labonneboite.conf import settings from labonneboite.common import mailjet, pro diff --git a/labonneboite/web/search/views.py b/labonneboite/web/search/views.py index 6f736ba65..af03db3d0 100644 --- a/labonneboite/web/search/views.py +++ b/labonneboite/web/search/views.py @@ -327,7 +327,11 @@ def entreprises(): form = make_company_search_form(**form_kwargs) # Render different template if it's an ajax call - template = 'search/results.html' if not request.is_xhr else 'search/results_content.html' + is_xhr = False + request_xhr_key = request.headers.get('X-Requested-With') + if request_xhr_key and request_xhr_key == 'XMLHttpRequest': + is_xhr = True + template = 'search/results.html' if not is_xhr else 'search/results_content.html' activity_log_properties = dict( emploi=occupation, diff --git a/requirements.dev.in b/requirements.dev.in index 0958d2615..7f858eafd 100644 --- a/requirements.dev.in +++ b/requirements.dev.in @@ -15,7 +15,6 @@ Flask-DebugToolbar ipdb ipython pylint -pip-tools # testing # locust @@ -25,7 +24,7 @@ pip-tools ########## sqlalchemy-stubs -types-Flask<0.13 +types-Flask # *missing*: types-Flask-Script ### Flask assets. @@ -115,8 +114,7 @@ types-cryptography # *missing*: types-pyzmq # *missing*: types-remote-pdb -#Fix markupsafe issue 1.0 -types-markupsafe==1.1.1 +types-markupsafe #Impact retour emploi. # *Could not find a version that matches*: google-api-python-client-stubs diff --git a/requirements.dev.txt b/requirements.dev.txt index 87991f4bc..9407a955b 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -7,7 +7,7 @@ -e git+https://github.com/StartupsPoleEmploi/labonneboite-common.git@f0dadcbb79338522169e586a9f4ec0a750920d00#egg=labonneboite-common alembic==0.9.10 astroid==1.6.5 -attrs==21.4.0 # via flake8-mypy, pytest +attrs==22.1.0 # via flake8-mypy, pytest babel==2.6.0 backcall==0.1.0 blinker==1.4 @@ -15,10 +15,11 @@ cachetools==4.0.0 certifi==2017.4.17 cffi==1.13.1 chardet==3.0.4 -click==6.7 +click==8.0.4 coverage==6.2 cryptography==2.8 cssmin==0.2.0 +dataclasses==0.8 decorator==4.3.0 defusedxml==0.5.0 easyprocess==0.3 @@ -26,17 +27,17 @@ elasticsearch-stubs==0 elasticsearch==1.9.0 first==2.0.1 flake8-mypy==17.8.0 -flake8==4.0.1 -flask-admin==1.5.3 +flake8==4.0.0 +flask-admin==1.6.0 flask-assets==0.12 -flask-babelex==0.9.3 -flask-cors==3.0.7 -flask-debugtoolbar==0.10.1 +flask-babelex==0.9.4 +flask-cors==3.0.10 +flask-debugtoolbar==0.13.1 flask-login==0.4.1 flask-script==2.0.6 -flask-testing==0.7.1 -flask-wtf==0.14.2 -flask==0.12.4 +flask-testing==0.8.1 +flask-wtf==1.0.1 +flask==2.0.3 future==0.16.0 geographiclib==1.49 geopy==1.19.0 @@ -49,32 +50,31 @@ greenlet==0.4.12 html5lib==1.0.1 httplib2==0.11.3 idna==2.5 -importlib-metadata==4.2.0 # via flake8, pluggy, pytest +importlib-metadata==4.2.0 iniconfig==1.1.1 # via pytest ipdb==0.13.9 ipython-genutils==0.2.0 ipython==7.16.1 isort==4.2.15 -itsdangerous==0.24 +itsdangerous==2.0.1 jedi==0.12.0 -jinja2==2.10.1 +jinja2==3.0.3 jsmin==3.0.0 lazy-object-proxy==1.3.1 line-profiler==2.0 locustio==0.7.5 mailjet-rest==1.3.3 mako==1.0.7 -markupsafe==1.1.1 +markupsafe==2.0.1 mccabe==0.6.1 msgpack-python==0.5.6 mypy-extensions==0.4.3 # via mypy -mypy==0.931 +mypy==0.971 mysqlclient==1.4.2.post1 nose==1.3.7 numpy==1.16.1 oauthlib==2.0.2 packaging==21.3 # via pytest -pandas-stubs==1.2.0.62 pandas==0.22.0 parameterized==0.7.0 parso==0.2.1 @@ -96,11 +96,11 @@ pygments==2.2.0 pyjwkest==1.4.0 pyjwt==1.5.2 pylint==1.9.2 -pyparsing==3.0.6 # via packaging +pyparsing==3.0.9 # via packaging pypdf2==1.26.0 pyprof2calltree==1.4.3 pytest-env==0.6.2 -pytest==6.2.5 +pytest==7.0.1 python-dateutil==2.6.1 python-editor==1.0.3 python-slugify==1.2.5 @@ -127,24 +127,22 @@ sqlalchemy-utils==0.32.13 sqlalchemy2-stubs==0.0.2a27 sqlalchemy==1.3.3 toml==0.10.2 -tomli==1.2.3 # via mypy +tomli==1.2.3 # via mypy, pytest traitlets==4.3.2 -typed-ast==1.5.1 # via mypy +typed-ast==1.5.4 # via mypy types-click==7.1.8 # via types-flask -types-cryptography==3.3.12 -types-enum34==1.1.2 # via types-cryptography -types-flask==0.1.2 -types-html5lib==1.1.5 -types-ipaddress==1.0.2 # via types-cryptography +types-cryptography==3.3.23 +types-flask==1.1.6 +types-html5lib==1.1.10 types-jinja2==2.11.9 # via types-flask -types-markupsafe==1.1.1 -types-mysqlclient==2.0.4 -types-python-slugify==5.0.3 -types-requests==2.27.6 -types-selenium==3.141.7 -types-urllib3==1.26.6 # via types-requests +types-markupsafe==1.1.10 +types-mysqlclient==2.1.5 +types-python-slugify==6.1.0 +types-requests==2.28.9 +types-selenium==3.141.9 +types-urllib3==1.26.23 # via types-requests types-werkzeug==1.0.9 # via types-flask -typing-extensions==4.0.1 # via importlib-metadata, mypy, pandas-stubs, sqlalchemy-stubs, sqlalchemy2-stubs +typing-extensions==4.1.1 unidecode==0.4.21 uritemplate==3.0.1 urllib3==1.24.3 @@ -154,7 +152,7 @@ validators==0.11.2 wcwidth==0.1.7 webassets==0.12.1 webencodings==0.5.1 -werkzeug==0.11.10 +werkzeug==2.0.3 wrapt==1.10.10 wtforms==2.1 xhtml2pdf==0.2.2 diff --git a/requirements.in b/requirements.in index 407153087..fe299814f 100644 --- a/requirements.in +++ b/requirements.in @@ -1,6 +1,6 @@ -e git+https://github.com/StartupsPoleEmploi/labonneboite-common.git@f0dadcbb79338522169e586a9f4ec0a750920d00#egg=labonneboite-common -Flask<0.13 +Flask Flask-Script # Flask assets. @@ -28,6 +28,9 @@ ipython pylint pip-tools +# force importlib version for flake8 compatibility +importlib-metadata<4.3 + # profiling tools used in create_index.py # pycallgraph pyprof2calltree @@ -88,7 +91,7 @@ pyzmq remote-pdb #Fix markupsafe issue 1.0 -markupsafe==1.1.1 +markupsafe #Impact retour emploi. google-api-python-client diff --git a/requirements.txt b/requirements.txt index 1cb01dc8c..4a18b4910 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,24 +14,25 @@ cachetools==4.0.0 # via google-auth certifi==2017.4.17 # via requests, sentry-sdk cffi==1.13.1 # via cryptography chardet==3.0.4 # via requests -click==6.7 # via flask, pip-tools +click==8.0.4 # via flask, pip-tools cryptography==2.8 cssmin==0.2.0 +dataclasses==0.8 # via werkzeug decorator==4.3.0 # via ipdb, ipython, traitlets, validators defusedxml==0.5.0 # via python3-openid, social-auth-core easyprocess==0.3 # via pyvirtualdisplay elasticsearch==1.9.0 first==2.0.1 # via pip-tools -flask-admin==1.5.3 +flask-admin==1.6.0 flask-assets==0.12 -flask-babelex==0.9.3 -flask-cors==3.0.7 -flask-debugtoolbar==0.10.1 +flask-babelex==0.9.4 +flask-cors==3.0.10 +flask-debugtoolbar==0.13.1 flask-login==0.4.1 flask-script==2.0.6 -flask-testing==0.7.1 -flask-wtf==0.14.2 -flask==0.12.4 +flask-testing==0.8.1 +flask-wtf==1.0.1 +flask==2.0.3 future==0.16.0 # via pyjwkest geographiclib==1.49 # via geopy geopy==1.19.0 @@ -44,27 +45,26 @@ greenlet==0.4.12 # via gevent html5lib==1.0.1 httplib2==0.11.3 # via google-api-python-client, google-auth-httplib2, xhtml2pdf idna==2.5 # via requests +importlib-metadata==4.2.0 ipdb==0.13.9 ipython-genutils==0.2.0 # via traitlets ipython==7.16.1 isort==4.2.15 # via pylint -itsdangerous==0.24 # via flask, flask-debugtoolbar +itsdangerous==2.0.1 # via flask, flask-debugtoolbar, flask-wtf jedi==0.12.0 # via ipython -jinja2==2.10.1 # via flask, flask-babelex +jinja2==3.0.3 # via flask, flask-babelex jsmin==3.0.0 lazy-object-proxy==1.3.1 # via astroid line-profiler==2.0 locustio==0.7.5 mailjet-rest==1.3.3 mako==1.0.7 # via alembic -markupsafe==1.1.1 +markupsafe==2.0.1 mccabe==0.6.1 # via pylint msgpack-python==0.5.6 # via locustio mysqlclient==1.4.2.post1 nose==1.3.7 -numpy==1.16.1 # via pandas oauthlib==2.0.2 # via requests-oauthlib, social-auth-core -pandas==0.22.0 parameterized==0.7.0 parso==0.2.1 # via jedi pexpect==4.6.0 # via ipython @@ -83,11 +83,11 @@ pyjwt==1.5.2 # via social-auth-core pylint==1.9.2 pypdf2==1.26.0 # via xhtml2pdf pyprof2calltree==1.4.3 -python-dateutil==2.6.1 # via alembic, pandas +python-dateutil==2.6.1 # via alembic python-editor==1.0.3 # via alembic python-slugify==1.2.5 python3-openid==3.1.0 # via social-auth-core -pytz==2017.2 # via babel, pandas +pytz==2017.2 # via babel pyvirtualdisplay==2.2 pyzmq==16.0.2 raven[flask]==6.9.0 @@ -108,6 +108,7 @@ sqlalchemy-utils==0.32.13 sqlalchemy==1.3.3 toml==0.10.2 # via ipdb traitlets==4.3.2 # via ipython +typing-extensions==4.1.1 # via importlib-metadata unidecode==0.4.21 # via python-slugify uritemplate==3.0.1 # via google-api-python-client urllib3==1.24.3 # via elasticsearch, requests, selenium, sentry-sdk @@ -117,7 +118,7 @@ validators==0.11.2 wcwidth==0.1.7 # via prompt-toolkit webassets==0.12.1 # via flask-assets webencodings==0.5.1 # via html5lib -werkzeug==0.11.10 # via flask, flask-debugtoolbar +werkzeug==2.0.3 # via flask, flask-debugtoolbar wrapt==1.10.10 # via astroid wtforms==2.1 xhtml2pdf==0.2.2 From 3e2a8440fbcd070b6b960af2529da9e46f9d483b Mon Sep 17 00:00:00 2001 From: sylvaintouret Date: Tue, 20 Sep 2022 11:48:17 +0200 Subject: [PATCH 2/7] dockerfile works! --- Dockerfile | 77 ++++----- docker-compose.yml | 112 +++++++------ .../conf/common/overrides/development.py | 12 +- .../common/conf/common/settings_common.py | 155 ++++++++++-------- labonneboite/common/database.py | 47 +++--- labonneboite/wsgi.py | 10 ++ requirements.txt | 1 + setup.py | 27 +-- 8 files changed, 243 insertions(+), 198 deletions(-) create mode 100644 labonneboite/wsgi.py diff --git a/Dockerfile b/Dockerfile index 907b9f38e..5bd8e7816 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,40 +1,43 @@ -FROM ubuntu:18.04 - +FROM python:3.6.15-slim-buster +# FROM python:3.10.7-slim-bullseye # Set timezone ENV TZ=Europe/Paris - -# Install system requirements ENV LANG C.UTF-8 -RUN apt update && \ - DEBIAN_FRONTEND=noninteractive apt install -y \ - git \ - libmysqlclient-dev \ - mysql-client \ - language-pack-fr \ - python3 python3-dev python3-pip \ - tzdata \ - # dependency required for travis - libssl-dev \ - # scipy - gfortran libblas-dev liblapack-dev libatlas-base-dev \ - && pip3 install virtualenv - -# Install python requirements -RUN mkdir -p /labonneboite/logs /labonneboite/src /labonneboite/jenkins -WORKDIR /labonneboite/src -COPY requirements.txt . -RUN virtualenv ../env && \ - ../env/bin/pip install -r requirements.txt - -# Copy and install code -COPY . /labonneboite/src -RUN git clean -xfd -RUN ../env/bin/pip install -e . - -# Generate static assets -ENV FLASK_APP labonneboite.web.app -RUN ../env/bin/flask assets build - -# Run uwsgi -EXPOSE 8000 -CMD ["../env/bin/uwsgi", "./docker/uwsgi.ini"] \ No newline at end of file +# for mysql support & git +RUN apt update && apt install -y \ + git \ + python3-dev \ + default-libmysqlclient-dev \ + build-essential \ + locales \ + # language-pack-fr \ + --no-install-recommends +# switch working directory +WORKDIR /app + +RUN mkdir -p /app/logs /app/src /app/jenkins + +# install gunicorn +RUN pip install gunicorn + +# copy the requirements file into the image +COPY ./requirements.txt /app/requirements.txt + +# install the dependencies and packages in the requirements file +RUN pip install -r /app/requirements.txt + +COPY setup* /app/ +COPY README.md /app/README.md +COPY ./labonneboite /app/labonneboite +RUN pip install -e . + +WORKDIR /app/labonneboite +ENV FLASK_APP web.app +# unsupported local error : https://stackoverflow.com/questions/54802935/docker-unsupported-locale-setting-when-running-python-container +RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \ + && sed -i -e 's/# fr_FR.UTF-8 UTF-8/fr_FR.UTF-8 UTF-8/' /etc/locale.gen \ + && locale-gen + +RUN flask assets build + +CMD ["gunicorn", "--workers", "2", "--bind", "0.0.0.0:8080", "web.app:app"] diff --git a/docker-compose.yml b/docker-compose.yml index 13f60e3de..136ea376f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,20 +1,30 @@ -version: "3" +version: "3.7" + +x-common-env: &cenv + MYSQL_ROOT_PASSWORD: p@ssword + MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' + MYSQL_DATABASE: labonneboite + MYSQL_USER: labonneboite + MYSQL_PASSWORD: labonneboite + + DB_ROOT_PASSWORD: p@ssword + DB_DATABASE: labonneboite + DB_NAME: labonneboite + DB_USER: labonneboite + DB_PASSWORD: labonneboite + DB_PORT: 3306 + ES_HOST: elasticsearch:9200 + DB_HOST: mysql + OFFICE_TABLE: etablissements + LBB_ENV: development + services: - nginx: - restart: always - image: nginx:1.13 - volumes: - - ./nginx/etc/nginx/conf.d:/etc/nginx/conf.d - - ./nginx/var/log/labonneboite:/var/log/nginx/labonneboite - ports: - - ${HTTP_PORT:-8000}:8000 - depends_on: - - labonneboite - ###### 3rd-party services - # https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html - # https://www.elastic.co/blog/how-to-make-a-dockerfile-for-elasticsearch + # ###### 3rd-party services + # # https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html + # # https://www.elastic.co/blog/how-to-make-a-dockerfile-for-elasticsearch elasticsearch: + hostname: elasticsearch image: elasticsearch:1.7.2 environment: - "ES_JAVA_OPTS=-Xms4g -Xmx4g" @@ -24,68 +34,56 @@ services: memlock: soft: -1 hard: -1 - #restart: "unless-stopped" ports: - # TODO remap port - 9200:9200 - 9300:9300 - networks: - - labonneboite_elk - - labonneboite_default + # networks: + # - labonneboite_elk + # - labonneboite_default + healthcheck: + test: curl --fail localhost:9200/_cat/health || exit 1 + interval: 10s + retries: 5 + timeout: 2s - kibana: - image: kibana:1.7.2 - environment: - - "ES_JAVA_OPTS=-Xms1g -Xmx1g" - - "cluster.name=lbb" - - "bootstrap.memory_lock=true" - - "vm.max_map_count=4g" - ports: - - 5601:5601 - networks: - - labonneboite_elk - depends_on: - - elasticsearch mysql: image: mysql:5.6.36 command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci #restart: unless-stopped environment: - MYSQL_ROOT_PASSWORD: '' - MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' + <<: *cenv ports: - 3306:3306 - networks: - - labonneboite_default + # networks: + # - labonneboite_default + healthcheck: + test: mysql ${MYSQL_DATABASE} --user=${MYSQL_USER} --password='${MYSQL_PASSWORD}' --silent --execute "SELECT 1;" + interval: 10s + retries: 5 + timeout: 2s - ###### Je Postule labonneboite: + hostname: labonneboite-backend restart: always build: context: . + environment: + <<: *cenv volumes: - ./labonneboite/logs:/labonneboite/logs - .:/labonneboite/src depends_on: - - elasticsearch - - mysql - expose: - - 8000 - # backups: - # restart: always - # build: - # context: . - # dockerfile: dockerfile_backups - # args: - # - ENV_TYPE=${ENV_TYPE:-production} - # - TIMEZONE=${TIMEZONE:-Europe/Paris} - # hostname: backups - # volumes: - # - ./:/home/docker - # - ./backups/home/backups:/home/backups - # - /mnt:/mnt + elasticsearch: + condition: service_healthy + mysql: + condition: service_healthy + ports: + - 8080:8080 + # networks: + # - labonneboite_elk + # - labonneboite_default -networks: - labonneboite_elk: - labonneboite_default: +# networks: +# labonneboite_elk: +# labonneboite_default: diff --git a/labonneboite/common/conf/common/overrides/development.py b/labonneboite/common/conf/common/overrides/development.py index 22d1bb2dd..25449d255 100644 --- a/labonneboite/common/conf/common/overrides/development.py +++ b/labonneboite/common/conf/common/overrides/development.py @@ -3,17 +3,17 @@ DEBUG = True SERVER_NAME = None -DB_HOST = '127.0.0.1' +# DB_HOST = '127.0.0.1' DB_PORT = 3306 LOG_FORMAT_USER_ACTIVITY = ( - '-' * 80 + '\n' + - '%(levelname)s in %(module)s [%(pathname)s:%(lineno)d]:\n' + - '%(message)s\n' + - '-' * 80 + "-" * 80 + + "\n" + + "%(levelname)s in %(module)s [%(pathname)s:%(lineno)d]:\n" + + "%(message)s\n" + + "-" * 80 ) PEAM_VERIFY_SSL = False SENTRY_ENVIRONMENT = "development" - diff --git a/labonneboite/common/conf/common/settings_common.py b/labonneboite/common/conf/common/settings_common.py index a7bd41460..5db2d7fd1 100644 --- a/labonneboite/common/conf/common/settings_common.py +++ b/labonneboite/common/conf/common/settings_common.py @@ -12,25 +12,37 @@ import logging import os -from labonneboite.common.env import get_current_env, ENV_BONAPARTE, ENV_DEVELOPMENT, ENV_TEST -from labonneboite.common.load_data import load_rome_labels, load_naf_labels, load_rows_as_set, load_csv_file +from labonneboite.common.env import ( + get_current_env, + ENV_BONAPARTE, + ENV_DEVELOPMENT, + ENV_TEST, +) +from labonneboite.common.load_data import ( + load_rome_labels, + load_naf_labels, + load_rows_as_set, + load_csv_file, +) DEBUG = False TESTING = False LOG_LEVEL = logging.INFO LOG_LEVEL_DB_ENGINE = logging.WARNING -LOG_API_ID = 'labonneboite.pole-emploi.fr' +LOG_API_ID = "labonneboite.pole-emploi.fr" LOG_LEVEL_USER_ACTIVITY = logging.INFO LOGGING_HANDLER_USER_ACTIVITY = logging.StreamHandler() LOGGING_HANDLER_API_ACTIVITY = logging.StreamHandler() -LOG_FORMAT_USER_ACTIVITY = '%(message)s' +LOG_FORMAT_USER_ACTIVITY = "%(message)s" -GLOBAL_STATIC_PATH = '/tmp' +GLOBAL_STATIC_PATH = "/tmp" # Related ROMEs suggestions -ENABLE_RELATED_ROMES = True # set this to False to deactivate the related romes mechanism +ENABLE_RELATED_ROMES = ( + True # set this to False to deactivate the related romes mechanism +) MAX_RELATED_ROMES = 5 ROME_DESCRIPTIONS = load_rome_labels() @@ -44,14 +56,14 @@ SENTRY_DSN = None SENTRY_ENVIRONMENT = "" -SENTRY_SAMPLE_RATE = 0.1 # set to 0 to disable sentry performance monitoring, @see https://docs.sentry.io/platforms/python/guides/flask/performance/#configure-the-sample-rate +SENTRY_SAMPLE_RATE = 0.1 # set to 0 to disable sentry performance monitoring, @see https://docs.sentry.io/platforms/python/guides/flask/performance/#configure-the-sample-rate -ADMIN_EMAIL = 'no-reply@labonneboite.pole-emploi.fr' -LBB_EMAIL = 'labonneboite@pole-emploi.fr' -LBA_EMAIL = 'labonnealternance@pole-emploi.fr' +ADMIN_EMAIL = "no-reply@labonneboite.pole-emploi.fr" +LBB_EMAIL = "labonneboite@pole-emploi.fr" +LBA_EMAIL = "labonnealternance@pole-emploi.fr" -SERVER_NAME = 'labonneboite.pole-emploi.fr' -PREFERRED_URL_SCHEME = 'http' +SERVER_NAME = "labonneboite.pole-emploi.fr" +PREFERRED_URL_SCHEME = "http" COOKIE_SECURE = False WTF_CSRF_ENABLED = True @@ -83,24 +95,26 @@ # Headcount HEADCOUNT_INSEE = { - '00': '0 salarié', - '01': '1 ou 2 salariés', - '02': '3 à 5 salariés', - '03': '6 à 9 salariés', - '11': '10 à 19 salariés', - '12': '20 à 49 salariés', - '21': '50 à 99 salariés', - '22': '100 à 199 salariés', - '31': '200 à 249 salariés', - '32': '250 à 499 salariés', - '41': '500 à 999 salariés', - '42': '1 000 à 1 999 salariés', - '51': '2 000 à 4 999 salariés', - '52': '5 000 à 9 999 salariés', - '53': '10 000 salariés et plus', + "00": "0 salarié", + "01": "1 ou 2 salariés", + "02": "3 à 5 salariés", + "03": "6 à 9 salariés", + "11": "10 à 19 salariés", + "12": "20 à 49 salariés", + "21": "50 à 99 salariés", + "22": "100 à 199 salariés", + "31": "200 à 249 salariés", + "32": "250 à 499 salariés", + "41": "500 à 999 salariés", + "42": "1 000 à 1 999 salariés", + "51": "2 000 à 4 999 salariés", + "52": "5 000 à 9 999 salariés", + "53": "10 000 salariés et plus", } -HEADCOUNT_INSEE_CHOICES = [(key, value) for key, value in sorted(HEADCOUNT_INSEE.items())] +HEADCOUNT_INSEE_CHOICES = [ + (key, value) for key, value in sorted(HEADCOUNT_INSEE.items()) +] HEADCOUNT_WHATEVER = 1 HEADCOUNT_SMALL_ONLY = 2 @@ -111,65 +125,74 @@ HEADCOUNT_FILTER_DEFAULT = 1 HEADCOUNT_VALUES = { - 'all': HEADCOUNT_WHATEVER, - 'big': HEADCOUNT_BIG_ONLY, - 'small': HEADCOUNT_SMALL_ONLY + "all": HEADCOUNT_WHATEVER, + "big": HEADCOUNT_BIG_ONLY, + "small": HEADCOUNT_SMALL_ONLY, } # Databases -ES_INDEX = 'labonneboite' +ES_INDEX = "labonneboite" # Set ES_TIMEOUT environment variable to 0 to remove ES timeouts entirely -ES_TIMEOUT = int(os.environ.get('ES_TIMEOUT', 10)) or None -ES_HOST = 'localhost:9200' -DB_HOST = 'localhost' -DB_PORT = 3306 -DB_NAME = 'labonneboite' -DB_USER = 'labonneboite' -DB_PASSWORD = 'labonneboite' -OFFICE_TABLE = 'etablissements' +ES_TIMEOUT = int(os.environ.get("ES_TIMEOUT", 10)) or None +ES_HOST = "localhost:9200" +DB_HOST = os.environ.get("DB_HOST", "localhost") +DB_PORT = os.environ.get("DB_PORT", 3306) +DB_NAME = os.environ.get("DB_NAME", "labonneboite") +DB_USER = os.environ.get("DB_USER", "labonneboite") +DB_PASSWORD = os.environ.get("DB_PASSWORD", "labonneboite") +OFFICE_TABLE = os.environ.get("OFFICE_TABLE", "etablissements") # 2020-05-13: The tile API of OpenRouteService (api.openrouteservice.org) will be discontinued in June 2020. TILE_SERVER_URL = "http://openmapsurfer.uni-hd.de/tiles/roads/x={x}&y={y}&z={z}" ROME_NAF_PROBABILITY_CUTOFF = 0.05 -FLASK_SECRET_KEY = '' +FLASK_SECRET_KEY = "" -PEAM_AUTH_BASE_URL = 'https://authentification-candidat.pole-emploi.fr' -PEAM_AUTH_RECRUITER_BASE_URL = 'https://entreprise.pole-emploi.fr/' -PEAM_API_BASE_URL = 'https://api.emploi-store.fr' -PEAM_TOKEN_BASE_URL = 'https://entreprise.pole-emploi.fr' +PEAM_AUTH_BASE_URL = "https://authentification-candidat.pole-emploi.fr" +PEAM_AUTH_RECRUITER_BASE_URL = "https://entreprise.pole-emploi.fr/" +PEAM_API_BASE_URL = "https://api.emploi-store.fr" +PEAM_TOKEN_BASE_URL = "https://entreprise.pole-emploi.fr" PEAM_VERIFY_SSL = True -REMEMBER_ME_ARG_NAME = 'keep' +REMEMBER_ME_ARG_NAME = "keep" # Settings that need to be overwritten -PEAM_CLIENT_ID = '' -PEAM_CLIENT_SECRET = '' +PEAM_CLIENT_ID = "" +PEAM_CLIENT_SECRET = "" -MAILJET_API_KEY = '' -MAILJET_API_SECRET = '' -TO_EMAILS = [''] # this was named `FORM_EMAIL`, but it is confusing -FROM_EMAIL = '' +MAILJET_API_KEY = "" +MAILJET_API_SECRET = "" +TO_EMAILS = [""] # this was named `FORM_EMAIL`, but it is confusing +FROM_EMAIL = "" # URL of a script to load for tag management # Leave blank to disable -TAG_MANAGER_URL = '' +TAG_MANAGER_URL = "" -MEMO_URL = 'https://memo.pole-emploi.fr' # staging url is https://memo.beta.pole-emploi.fr -API_ADRESSE_BASE_URL = 'https://api-adresse.data.gouv.fr' -API_DEPARTMENTS_URL = 'https://geo.api.gouv.fr/departements' +MEMO_URL = ( + "https://memo.pole-emploi.fr" # staging url is https://memo.beta.pole-emploi.fr +) +API_ADRESSE_BASE_URL = "https://api-adresse.data.gouv.fr" +API_DEPARTMENTS_URL = "https://geo.api.gouv.fr/departements" # Je postule -JEPOSTULE_BASE_URL = 'http://127.0.0.1:8000' -JEPOSTULE_CLIENT_ID = '' -JEPOSTULE_CLIENT_SECRET = '' +JEPOSTULE_BASE_URL = "http://127.0.0.1:8000" +JEPOSTULE_CLIENT_ID = "" +JEPOSTULE_CLIENT_SECRET = "" JEPOSTULE_BETA_EMAILS = [] JEPOSTULE_QUOTA = 0 # Google site verification code - for linking with Google Search Console GOOGLE_SITE_VERIFICATION_CODE = None -SCAM_EMAILS_FOLDER = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..', '..', 'scripts', 'scam_emails') +SCAM_EMAILS_FOLDER = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "..", + "..", + "..", + "scripts", + "scam_emails", +) # Ids of spreadsheets for local dev # SPREADSHEET_IDS = [ @@ -180,15 +203,15 @@ # ] SPREADSHEET_IDS = { - "stats_ire": '197iFVyuCHiNNuA0ns99TCTS-v2CmBITg-e0ztEcwPMA', - "delay_activity_ire": '1Gl_rWicSmLwpXAPJLR3eRbs5nWJ1ROf6GSmUGmL2DEk', - "perf_indicators_lba": '1hPq1X_VXM7-l38jhLFrxQaDdpoB4s5W1Ok612SVTHts', - "perf_indicators_lbb": '1HFkQVLxzjT0zIACCCp8c6fJII1VieKDtdvdEkLLO16M' + "stats_ire": "197iFVyuCHiNNuA0ns99TCTS-v2CmBITg-e0ztEcwPMA", + "delay_activity_ire": "1Gl_rWicSmLwpXAPJLR3eRbs5nWJ1ROf6GSmUGmL2DEk", + "perf_indicators_lba": "1hPq1X_VXM7-l38jhLFrxQaDdpoB4s5W1Ok612SVTHts", + "perf_indicators_lbb": "1HFkQVLxzjT0zIACCCp8c6fJII1VieKDtdvdEkLLO16M", } # Encryption of user PEAM-U token between LBB and JePostule. # Dummy key used everywhere but in production. -CRYPTOGRAPHY_SECRET_KEY = b'gj6ouKvodK6PCAz4mt5tdTMUnVPHFFYWjh_P-O-IMqU=' +CRYPTOGRAPHY_SECRET_KEY = b"gj6ouKvodK6PCAz4mt5tdTMUnVPHFFYWjh_P-O-IMqU=" # The only case where you don't want this is when using # PE Connect on the staging ESD, where we do not have this @@ -213,7 +236,7 @@ # Mobiville MOBIVILLE_MAX_COMPANY_COUNT = 5 -MOBIVILLE_ROMES = load_rows_as_set(load_csv_file('mobiville/romes.csv')) +MOBIVILLE_ROMES = load_rows_as_set(load_csv_file("mobiville/romes.csv")) SCORE_REDUCING_MINIMUM_THRESHOLD = 50 # HIRING_REDUCING_MINIMUM_THRESHOLD = scoring.get_hirings_from_score(50) diff --git a/labonneboite/common/database.py b/labonneboite/common/database.py index 6548c872f..d8749a9d1 100644 --- a/labonneboite/common/database.py +++ b/labonneboite/common/database.py @@ -16,11 +16,11 @@ # ----------------------------------------------------------------------------- DATABASE: Dict[str, Union[str, int, None]] = { - 'HOST': settings.DB_HOST, - 'PORT': settings.DB_PORT, - 'NAME': settings.DB_NAME, - 'USER': settings.DB_USER, - 'PASSWORD': settings.DB_PASSWORD, + "HOST": settings.DB_HOST, + "PORT": settings.DB_PORT, + "NAME": settings.DB_NAME, + "USER": settings.DB_USER, + "PASSWORD": settings.DB_PASSWORD, } @@ -32,22 +32,23 @@ def get_db_string(db_params: Optional[Dict[str, Union[str, int, None]]] = None) """ # Build the connection string db_params = db_params or DATABASE - str = "mysql://{USER}:{PASSWORD}@{HOST}:{PORT}/{NAME}?charset=utf8mb4".format(**db_params) + s = "mysql://{USER}:{PASSWORD}@{HOST}:{PORT}/{NAME}?charset=utf8mb4".format( + **db_params + ) # Add the optional param to enable `LOAD DATA LOCAL INFILE` SQL instructions - str = str + "&local_infile=1" if os.environ.get('ENABLE_DB_INFILE') else str - return str + s = s + "&local_infile=1" if os.environ.get("ENABLE_DB_INFILE") else s + print(s, flush=True) + return s pool_recycle = int(os.environ.get("DB_CONNECTION_TIMEOUT", "30")) connect_timeout = int(os.environ.get("CONNECT_TIMEOUT", "5")) ENGINE_PARAMS = { - 'convert_unicode': True, - 'echo': False, - 'pool_recycle': pool_recycle, - 'connect_args': { - 'connect_timeout': connect_timeout - } + "convert_unicode": True, + "echo": False, + "pool_recycle": pool_recycle, + "connect_args": {"connect_timeout": connect_timeout}, } engine = create_engine(get_db_string(), **ENGINE_PARAMS) @@ -61,20 +62,24 @@ def get_db_string(db_params: Optional[Dict[str, Union[str, int, None]]] = None) # http://www.dangtrinh.com/2014/03/i-got-this-error-when-trying-to.html _expire_on_commit = False -db_session = scoped_session(sessionmaker( - autocommit=False, - autoflush=False, - bind=engine, - expire_on_commit=_expire_on_commit, -)) +db_session = scoped_session( + sessionmaker( + autocommit=False, + autoflush=False, + bind=engine, + expire_on_commit=_expire_on_commit, + ) +) # Base # ----------------------------------------------------------------------------- if TYPE_CHECKING: + class Base(declarative_base()): # type: ignore query = db_session.query_property() + else: Base = declarative_base() Base.query = db_session.query_property() @@ -99,6 +104,7 @@ def init_db() -> None: # Imports are used by SQLAlchemy `metadata.create_all()` to know what tables to create. from social_flask_sqlalchemy.models import PSABase from social_flask_sqlalchemy.models import Nonce, Association, Code + # pylint:enable=unused-variable # InnoDB has a maximum index length of 767 bytes, so for utf8mb4 we can index a maximum of 191 characters. Code.email.property.columns[0].type.length = 191 @@ -118,6 +124,7 @@ def delete_db() -> None: # pylint:disable=unused-variable # Imports are used by SQLAlchemy `metadata.create_all()` to know what tables to create. from social_flask_sqlalchemy.models import PSABase + # pylint:enable=unused-variable PSABase.metadata.drop_all(engine) diff --git a/labonneboite/wsgi.py b/labonneboite/wsgi.py new file mode 100644 index 000000000..5eb970597 --- /dev/null +++ b/labonneboite/wsgi.py @@ -0,0 +1,10 @@ +from labonneboite.web import app +from labonneboite.common.env import ENV_DEVELOPMENT, get_current_env + +if __name__ == "__main__": + if get_current_env() == ENV_DEVELOPMENT: + # Since March 2020, PE Connect no longer allows redirect_uri with port 5000, however port 8080 works. + # Additionally 'localhost' works whereas '0.0.0.0' no longer does. + app.run(host="localhost", port=8080, debug=True) + else: + app.run(host="0.0.0.0") diff --git a/requirements.txt b/requirements.txt index 4a18b4910..408f6acc4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -123,3 +123,4 @@ wrapt==1.10.10 # via astroid wtforms==2.1 xhtml2pdf==0.2.2 zipp==3.6.0 +pandas \ No newline at end of file diff --git a/setup.py b/setup.py index 7cbb8aa7a..a0bda0391 100644 --- a/setup.py +++ b/setup.py @@ -7,17 +7,19 @@ def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() -pip_editable_with_egg_regex = re.compile('-e (.+#egg=(.+))') +pip_editable_with_egg_regex = re.compile("-e (.+#egg=(.+))") def requirement_to_install_require(requirement: str): result = pip_editable_with_egg_regex.match(requirement) if result: - return f'{result.group(2)} @ {result.group(1)}' + return f"{result.group(2)} @ {result.group(1)}" return requirement -install_requires = [requirement_to_install_require(req) for req in open('requirements.txt')] +install_requires = [ + requirement_to_install_require(req) for req in open("requirements.txt") +] setup( name="LaBonneBoite", @@ -26,21 +28,22 @@ def requirement_to_install_require(requirement: str): author_email="labonneboite@pole-emploi.fr", description=(""), packages=[ - 'labonneboite', + "labonneboite", ], include_package_data=True, - long_description=read('README.md'), + long_description=read("README.md"), install_requires=install_requires, entry_points={ - 'console_scripts': [ - 'create_index = labonneboite.scripts.create_index:run', + "console_scripts": [ + "create_index = labonneboite.scripts.create_index:run", ], }, classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'License :: OSI Approved :: GNU Affero General Public License v3', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: GNU Affero General Public License v3", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", ], ) From d0f5ce4885e9df78d2bd1c0c78e7be6dc5455e83 Mon Sep 17 00:00:00 2001 From: sylvaintouret Date: Tue, 20 Sep 2022 12:03:33 +0200 Subject: [PATCH 3/7] alembic, bdd works --- Dockerfile | 14 +++++++++---- alembic.ini | 2 +- docker-compose.yml | 50 +++++++++++++++++++++++----------------------- run.sh | 4 ++++ 4 files changed, 40 insertions(+), 30 deletions(-) create mode 100644 run.sh diff --git a/Dockerfile b/Dockerfile index 5bd8e7816..929478fb7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,18 +20,20 @@ RUN mkdir -p /app/logs /app/src /app/jenkins # install gunicorn RUN pip install gunicorn -# copy the requirements file into the image +# install dependencies COPY ./requirements.txt /app/requirements.txt - -# install the dependencies and packages in the requirements file RUN pip install -r /app/requirements.txt +# import files for finishing (SYTT: Could be done better) COPY setup* /app/ COPY README.md /app/README.md + COPY ./labonneboite /app/labonneboite RUN pip install -e . +# running the server WORKDIR /app/labonneboite + ENV FLASK_APP web.app # unsupported local error : https://stackoverflow.com/questions/54802935/docker-unsupported-locale-setting-when-running-python-container RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \ @@ -40,4 +42,8 @@ RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen RUN flask assets build -CMD ["gunicorn", "--workers", "2", "--bind", "0.0.0.0:8080", "web.app:app"] +# add the entrypoint +COPY alembic.ini /app/labonneboite/ +COPY run.sh /app/labonneboite/ +RUN chmod +x ./run.sh +ENTRYPOINT ["/bin/bash", "./run.sh"] diff --git a/alembic.ini b/alembic.ini index 7b86c5ccc..959ce6eca 100644 --- a/alembic.ini +++ b/alembic.ini @@ -2,7 +2,7 @@ [alembic] # Path to migration scripts. -script_location = labonneboite/alembic +script_location = ./alembic # Logging configuration. [loggers] diff --git a/docker-compose.yml b/docker-compose.yml index 136ea376f..93d2d21bd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,6 @@ version: "3.7" +# settings x-common-env: &cenv MYSQL_ROOT_PASSWORD: p@ssword MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' @@ -20,6 +21,24 @@ x-common-env: &cenv services: + labonneboite: + hostname: labonneboite-backend + restart: always + build: + context: . + environment: + <<: *cenv + volumes: + - ./labonneboite/logs:/labonneboite/logs + - .:/labonneboite/src + depends_on: + elasticsearch: + condition: service_healthy + mysql: + condition: service_healthy + ports: + - 8080:8080 + # ###### 3rd-party services # # https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html # # https://www.elastic.co/blog/how-to-make-a-dockerfile-for-elasticsearch @@ -46,44 +65,25 @@ services: retries: 5 timeout: 2s - + # database mysql: image: mysql:5.6.36 command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci - #restart: unless-stopped environment: <<: *cenv ports: - 3306:3306 - # networks: - # - labonneboite_default healthcheck: test: mysql ${MYSQL_DATABASE} --user=${MYSQL_USER} --password='${MYSQL_PASSWORD}' --silent --execute "SELECT 1;" interval: 10s retries: 5 timeout: 2s - labonneboite: - hostname: labonneboite-backend + # Dev tools + adminer: + image: adminer restart: always - build: - context: . - environment: - <<: *cenv - volumes: - - ./labonneboite/logs:/labonneboite/logs - - .:/labonneboite/src - depends_on: - elasticsearch: - condition: service_healthy - mysql: - condition: service_healthy ports: - - 8080:8080 - # networks: - # - labonneboite_elk - # - labonneboite_default + - 8000:8080 + -# networks: -# labonneboite_elk: -# labonneboite_default: diff --git a/run.sh b/run.sh new file mode 100644 index 000000000..73585bca2 --- /dev/null +++ b/run.sh @@ -0,0 +1,4 @@ +# /bin/bash +alembic upgrade head + +gunicorn --workers 2 --bind 0.0.0.0:8080 web.app:app \ No newline at end of file From 9b995e4ccfa2e51b4b15ee9955e2814d47f2dd9f Mon Sep 17 00:00:00 2001 From: sylvaintouret Date: Tue, 20 Sep 2022 16:08:15 +0200 Subject: [PATCH 4/7] restructured repo and added reload mode for local dev --- .gitignore | 1 + Dockerfile | 34 ++--- docker-compose.yml | 32 +++-- docker/uwsgi.ini | 47 ------- docker/v3.6/requirements.txt | 127 ++++++++++++++++++ docker/v3.6/setup.cfg | 94 +++++++++++++ docker/v3.6/setup.py | 48 +++++++ alembic.ini => labonneboite/alembic.ini | 0 .../common/conf/common/settings_common.py | 2 +- labonneboite/common/database.py | 1 - labonneboite/common/env.py | 10 +- labonneboite/run.sh | 3 + labonneboite/wsgi-conf.py | 10 ++ labonneboite/wsgi.py | 9 -- run.sh | 4 - 15 files changed, 317 insertions(+), 105 deletions(-) delete mode 100644 docker/uwsgi.ini create mode 100644 docker/v3.6/requirements.txt create mode 100644 docker/v3.6/setup.cfg create mode 100644 docker/v3.6/setup.py rename alembic.ini => labonneboite/alembic.ini (100%) create mode 100644 labonneboite/run.sh create mode 100644 labonneboite/wsgi-conf.py delete mode 100644 run.sh diff --git a/.gitignore b/.gitignore index 6c9532c4c..311987ced 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,4 @@ labonneboite/web/static/gen/ # virtualenv venv/ +logs/ diff --git a/Dockerfile b/Dockerfile index 929478fb7..dec6f98cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,49 +1,41 @@ FROM python:3.6.15-slim-buster -# FROM python:3.10.7-slim-bullseye + # Set timezone ENV TZ=Europe/Paris -ENV LANG C.UTF-8 -# for mysql support & git +ENV FLASK_APP web.app + +# for mysql support & git & french langage support RUN apt update && apt install -y \ git \ python3-dev \ default-libmysqlclient-dev \ build-essential \ locales \ - # language-pack-fr \ --no-install-recommends + # switch working directory WORKDIR /app +RUN mkdir -p /app/logs -RUN mkdir -p /app/logs /app/src /app/jenkins - -# install gunicorn -RUN pip install gunicorn - -# install dependencies -COPY ./requirements.txt /app/requirements.txt -RUN pip install -r /app/requirements.txt - -# import files for finishing (SYTT: Could be done better) -COPY setup* /app/ -COPY README.md /app/README.md +# Installing requirements +# COPY docker/v3.6/requirements.txt /requirements.txt +# RUN pip install -r /requirements.txt +# File imports : source code +COPY docker/v3.6/ /app/ COPY ./labonneboite /app/labonneboite RUN pip install -e . - -# running the server WORKDIR /app/labonneboite -ENV FLASK_APP web.app # unsupported local error : https://stackoverflow.com/questions/54802935/docker-unsupported-locale-setting-when-running-python-container RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \ && sed -i -e 's/# fr_FR.UTF-8 UTF-8/fr_FR.UTF-8 UTF-8/' /etc/locale.gen \ && locale-gen +# building flask assets RUN flask assets build # add the entrypoint -COPY alembic.ini /app/labonneboite/ -COPY run.sh /app/labonneboite/ RUN chmod +x ./run.sh + ENTRYPOINT ["/bin/bash", "./run.sh"] diff --git a/docker-compose.yml b/docker-compose.yml index 93d2d21bd..3d8551930 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,14 @@ version: "3.7" # settings -x-common-env: &cenv +x-db-env: &dbenv MYSQL_ROOT_PASSWORD: p@ssword MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' MYSQL_DATABASE: labonneboite MYSQL_USER: labonneboite MYSQL_PASSWORD: labonneboite +x-common-env: &cenv DB_ROOT_PASSWORD: p@ssword DB_DATABASE: labonneboite DB_NAME: labonneboite @@ -26,16 +27,16 @@ services: restart: always build: context: . + args: + options: --reload environment: - <<: *cenv + <<: *cenv volumes: - - ./labonneboite/logs:/labonneboite/logs - - .:/labonneboite/src + - ./logs:/app/labonneboite/logs + - ./labonneboite:/app/labonneboite depends_on: - elasticsearch: - condition: service_healthy - mysql: - condition: service_healthy + - elasticsearch + - mysql ports: - 8080:8080 @@ -56,9 +57,6 @@ services: ports: - 9200:9200 - 9300:9300 - # networks: - # - labonneboite_elk - # - labonneboite_default healthcheck: test: curl --fail localhost:9200/_cat/health || exit 1 interval: 10s @@ -70,14 +68,14 @@ services: image: mysql:5.6.36 command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci environment: - <<: *cenv + <<: *dbenv ports: - 3306:3306 - healthcheck: - test: mysql ${MYSQL_DATABASE} --user=${MYSQL_USER} --password='${MYSQL_PASSWORD}' --silent --execute "SELECT 1;" - interval: 10s - retries: 5 - timeout: 2s + # healthcheck: + # test: mysql ${MYSQL_DATABASE} --user=${MYSQL_USER} --password='${MYSQL_PASSWORD}' --silent --execute "SELECT 1;" + # interval: 10s + # retries: 5 + # timeout: 2s # Dev tools adminer: diff --git a/docker/uwsgi.ini b/docker/uwsgi.ini deleted file mode 100644 index 19d2bba97..000000000 --- a/docker/uwsgi.ini +++ /dev/null @@ -1,47 +0,0 @@ -[uwsgi] - -master = true -processes = 16 -http-socket = 0.0.0.0:8000 - -virtualenv = /labonneboite/env -chdir = /labonneboite/src -wsgi-file = labonneboite/web/app.py -callable = app - -# uwsgi should not start if the python app does not start in the first place -# see https://stackoverflow.com/questions/21826739/how-to-make-uwsgi-shut-down-if-application-failed-to-load -need-app = true - -# increase headers max buffer size (default 4096) otherwise PEAM requests fail -# with a puzzling 502 error as their headers are very large. -# such error can be seen only in uwsgi.log -# `[WARNING] unable to add ... to uwsgi packet, consider increasing buffer size` -# see https://uwsgi-docs.readthedocs.io/en/latest/Options.html#buffer-size -buffer-size = 8192 - -env = HOME=/labonneboite -env = LANG=fr_FR.UTF-8 - -honour-stdin = true -enable-threads = true -wsgi-disable-file-wrapper = true - -# Do not log "OSError: write error" exceptions that are caused by nginx -# read timeouts. -disable-write-exception = true - -# To monitor uwsgi in realtime even in production, -# go inside python container and run : -# ../env/bin/uwsgitop /tmp/stats.socket -stats = /tmp/stats.socket -memory-report = true - -logto = /labonneboite/logs/uwsgi.log -log-date = true -log-maxsize = 2000000 -log-backupname = /labonneboite/logs/uwsgi.log.backup -log-reopen = true - -# Default log format with additional host info http://uwsgi-docs.readthedocs.io/en/latest/LogFormat.html#uwsgi-default-logging -log-format = [pid: %(pid)|app: -|req: -/-|host: %(host)] %(addr) (%(user)) {%(vars) vars in %(pktsize) bytes} [%(ctime)] %(method) %(uri) => generated %(rsize) bytes in %(msecs) msecs (%(proto) %(status)) %(headers) headers in %(hsize) bytes (%(switches) switches on core %(core)) diff --git a/docker/v3.6/requirements.txt b/docker/v3.6/requirements.txt new file mode 100644 index 000000000..55f119a68 --- /dev/null +++ b/docker/v3.6/requirements.txt @@ -0,0 +1,127 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --output-file requirements.txt requirements.in +# +-e git+https://github.com/StartupsPoleEmploi/labonneboite-common.git@f0dadcbb79338522169e586a9f4ec0a750920d00#egg=labonneboite-common +alembic==0.9.10 +astroid==1.6.5 # via pylint +babel==2.6.0 # via flask-babelex +backcall==0.1.0 # via ipython +blinker==1.4 # via flask-debugtoolbar, raven +cachetools==4.0.0 # via google-auth +certifi==2017.4.17 # via requests, sentry-sdk +cffi==1.13.1 # via cryptography +chardet==3.0.4 # via requests +click==8.0.4 # via flask, pip-tools +cryptography==2.8 +cssmin==0.2.0 +dataclasses==0.8 # via werkzeug +decorator==4.3.0 # via ipdb, ipython, traitlets, validators +defusedxml==0.5.0 # via python3-openid, social-auth-core +easyprocess==0.3 # via pyvirtualdisplay +elasticsearch==1.9.0 +first==2.0.1 # via pip-tools +flask-admin==1.6.0 +flask-assets==0.12 +flask-babelex==0.9.4 +flask-cors==3.0.10 +flask-debugtoolbar==0.13.1 +flask-login==0.4.1 +flask-script==2.0.6 +flask-testing==0.8.1 +flask-wtf==1.0.1 +flask==2.0.3 +future==0.16.0 # via pyjwkest +geographiclib==1.49 # via geopy +geopy==1.19.0 +gevent==1.1.1 # via locustio +google-api-python-client==1.7.11 +google-auth-httplib2==0.0.3 +google-auth-oauthlib==0.4.1 +google-auth==1.11.0 # via google-api-python-client, google-auth-httplib2, google-auth-oauthlib +greenlet==0.4.12 # via gevent +html5lib==1.0.1 +httplib2==0.11.3 # via google-api-python-client, google-auth-httplib2, xhtml2pdf +idna==2.5 # via requests +importlib-metadata==4.2.0 +ipdb==0.13.9 +ipython-genutils==0.2.0 # via traitlets +ipython==7.16.1 +isort==4.2.15 # via pylint +itsdangerous==2.0.1 # via flask, flask-debugtoolbar, flask-wtf +jedi==0.12.0 # via ipython +jinja2==3.0.3 # via flask, flask-babelex +jsmin==3.0.0 +lazy-object-proxy==1.3.1 # via astroid +line-profiler==2.0 +locustio==0.7.5 +mailjet-rest==1.3.3 +mako==1.0.7 # via alembic +markupsafe==2.0.1 +mccabe==0.6.1 # via pylint +msgpack-python==0.5.6 # via locustio +mysqlclient==1.4.2.post1 +nose==1.3.7 +oauthlib==2.0.2 # via requests-oauthlib, social-auth-core +parameterized==0.7.0 +parso==0.2.1 # via jedi +pexpect==4.6.0 # via ipython +pickleshare==0.7.4 # via ipython +pillow==6.0.0 # via reportlab, xhtml2pdf +pip-tools==2.0.2 +prompt-toolkit==3.0.21 # via ipython +ptyprocess==0.6.0 # via pexpect +pyasn1-modules==0.2.8 # via google-auth +pyasn1==0.4.8 # via pyasn1-modules, rsa +pycparser==2.19 # via cffi +pycryptodomex==3.6.3 # via pyjwkest +pygments==2.2.0 # via ipython +pyjwkest==1.4.0 # via social-auth-core +pyjwt==1.5.2 # via social-auth-core +pylint==1.9.2 +pypdf2==1.26.0 # via xhtml2pdf +pyprof2calltree==1.4.3 +python-dateutil==2.6.1 # via alembic +python-editor==1.0.3 # via alembic +python-slugify==1.2.5 +python3-openid==3.1.0 # via social-auth-core +pytz==2017.2 # via babel +pyvirtualdisplay==2.2 +pyzmq==16.0.2 +raven[flask]==6.9.0 +remote-pdb==1.3.0 +reportlab==3.5.21 # via xhtml2pdf +requests-oauthlib==0.8.0 # via google-auth-oauthlib, social-auth-core +requests==2.21.0 +rsa==4.0 # via google-auth +selenium==3.141.0 +sentry-sdk==0.20.3 +six==1.10.0 # via astroid, cryptography, flask-cors, google-api-python-client, google-auth, html5lib, pip-tools, pyjwkest, pylint, python-dateutil, social-auth-app-flask, social-auth-app-flask-sqlalchemy, social-auth-core, social-auth-storage-sqlalchemy, sqlalchemy-utils, traitlets, validators, xhtml2pdf +social-auth-app-flask-sqlalchemy==1.0.1 +social-auth-app-flask==1.0.0 +social-auth-core[openidconnect]==1.4.0 +social-auth-storage-sqlalchemy==1.1.0 # via social-auth-app-flask-sqlalchemy +speaklater==1.3 +sqlalchemy-utils==0.32.13 +sqlalchemy==1.3.3 +toml==0.10.2 # via ipdb +traitlets==4.3.2 # via ipython +typing-extensions==4.1.1 # via importlib-metadata +unidecode==0.4.21 # via python-slugify +uritemplate==3.0.1 # via google-api-python-client +urllib3==1.24.3 # via elasticsearch, requests, selenium, sentry-sdk +uwsgi==2.0.18 +uwsgitop==0.11 +validators==0.11.2 +wcwidth==0.1.7 # via prompt-toolkit +webassets==0.12.1 # via flask-assets +webencodings==0.5.1 # via html5lib +werkzeug==2.0.3 # via flask, flask-debugtoolbar +wrapt==1.10.10 # via astroid +wtforms==2.1 +xhtml2pdf==0.2.2 +zipp==3.6.0 +pandas==1.0.5 +gunicorn==20.1.0 \ No newline at end of file diff --git a/docker/v3.6/setup.cfg b/docker/v3.6/setup.cfg new file mode 100644 index 000000000..2ed843971 --- /dev/null +++ b/docker/v3.6/setup.cfg @@ -0,0 +1,94 @@ +################################################## +# coverage # +################################################## +[coverage:run] +branch = True +parallel = true +concurrency=multiprocessing + +[coverage:report] +precision = 1 +show_missing = True +ignore_errors = True +exclude_lines = + pragma: no cover + raise NotImplementedError + def __repr__ + if settings.DEBUG + if __name__ == .__main__.: + if TYPE_CHECKING: +omit = + */test* + */migrations/* + manage.py + venv/* + +################################################## +# flake8 # +################################################## +[flake8] +ignore = + W503, # line break before binary operator + D100, # Missing docstring in public module + D101, # Missing docstring in public class + D102, # Missing docstring in public method + D104, # Missing docstring in public package + D106, # Missing docstring in public nested class + D200, # One-line docstring should fit on one line with quotes + D202, # No blank lines allowed after function docstring + D204, # 1 blank line required after class docstring + D205, # 1 blank line required between summary line and description + D400, # First line should end with a period + D406 # 1 blank line required before class docstring + +max-line-length = 120 +max-complexity = 18 +select = B,C,E,F,W,T4,B9 +enable-extensions = I,A,G,D +application-import-names = api,config,post,model_utils,saas,appointment,user +import-order-style = google +docstring-convention = numpy +exclude = migrations + +################################################## +# isort # +################################################## +[isort] +multi_line_output = 2 +line_length = 120 +order_by_type = false +force_to_top = labonneboite.conf + +################################################## +# mypy # +################################################## +[mypy] +ignore_missing_imports = false +follow_imports = silent +# no_strict_optional = true +show_error_codes = true +plugins = sqlmypy +disable_error_code = no-untyped-call +strict = true + +[mypy-alembic.*,astroid.*,flask_admin.*,dateutil.*,sqlalchemy_utils.*,babel.dates.*,easyprocess.*,elasticsearch.*,elasticsearch.exceptions.*,elasticsearch.helpers.*,flask_admin.*,flask_admin.contrib.sqla.*,flask_assets.*,flask_babelex.*,flask_debugtoolbar.*,flask.ext.cors.*,flask_login.*,flask_script.*,flask_testing.*,flask_wtf.*,flask_wtf.csrf.*,geopy.*,geopy.distance.*,googleapiclient.discovery.*,google_auth_oauthlib.flow.*,google.auth.transport.requests.*,ipdb.*,locust.*,mailjet_rest.*,MySQLdb.*,numpy.*,pandas.*,parameterized.*,pyprof2calltree.*,pyvirtualdisplay.*,sklearn.*,sklearn.metrics.*,social_core.*,social_core.backends.open_id_connect.*,social_core.exceptions.*,social_flask.routes.*,social_flask_sqlalchemy.models.*,social_flask.utils.*,unidecode.*,validators.*,wtforms.*,wtforms.fields.html5.*,wtforms.validators.*,wtforms.widgets.*,xhtml2pdf.*] +ignore_missing_imports = true + +################################################## +# yapf # +################################################## +[yapf] +based_on_style = google +column_limit = 120 +split_before_logical_operator = true +split_before_dot = true +coalesce_brackets = true +align_closing_bracket_with_visual_indent = true +allow_split_before_dict_value = false +blank_line_before_nested_class_or_def = true +blank_lines_around_top_level_definition = 2 + +[tool:pytest] +python_files = test_*.py +env = + LBB_ENV=test \ No newline at end of file diff --git a/docker/v3.6/setup.py b/docker/v3.6/setup.py new file mode 100644 index 000000000..6b5a1bd34 --- /dev/null +++ b/docker/v3.6/setup.py @@ -0,0 +1,48 @@ +import os +from setuptools import setup +import re + + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + + +pip_editable_with_egg_regex = re.compile("-e (.+#egg=(.+))") + + +def requirement_to_install_require(requirement: str): + result = pip_editable_with_egg_regex.match(requirement) + if result: + return f"{result.group(2)} @ {result.group(1)}" + return requirement + + +install_requires = [ + requirement_to_install_require(req) for req in open("requirements.txt") +] + +setup( + name="LaBonneBoite", + version="0.1", + author="La Bonne Boite", + author_email="labonneboite@pole-emploi.fr", + description=(""), + packages=[ + "labonneboite", + ], + include_package_data=True, + install_requires=install_requires, + entry_points={ + "console_scripts": [ + "create_index = labonneboite.scripts.create_index:run", + ], + }, + classifiers=[ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: GNU Affero General Public License v3", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + ], +) diff --git a/alembic.ini b/labonneboite/alembic.ini similarity index 100% rename from alembic.ini rename to labonneboite/alembic.ini diff --git a/labonneboite/common/conf/common/settings_common.py b/labonneboite/common/conf/common/settings_common.py index 5db2d7fd1..253de2452 100644 --- a/labonneboite/common/conf/common/settings_common.py +++ b/labonneboite/common/conf/common/settings_common.py @@ -134,7 +134,7 @@ ES_INDEX = "labonneboite" # Set ES_TIMEOUT environment variable to 0 to remove ES timeouts entirely ES_TIMEOUT = int(os.environ.get("ES_TIMEOUT", 10)) or None -ES_HOST = "localhost:9200" +ES_HOST = os.environ.get("ES_HOST", "localhost:9200") DB_HOST = os.environ.get("DB_HOST", "localhost") DB_PORT = os.environ.get("DB_PORT", 3306) DB_NAME = os.environ.get("DB_NAME", "labonneboite") diff --git a/labonneboite/common/database.py b/labonneboite/common/database.py index d8749a9d1..30fe61ef2 100644 --- a/labonneboite/common/database.py +++ b/labonneboite/common/database.py @@ -37,7 +37,6 @@ def get_db_string(db_params: Optional[Dict[str, Union[str, int, None]]] = None) ) # Add the optional param to enable `LOAD DATA LOCAL INFILE` SQL instructions s = s + "&local_infile=1" if os.environ.get("ENABLE_DB_INFILE") else s - print(s, flush=True) return s diff --git a/labonneboite/common/env.py b/labonneboite/common/env.py index e2a3390e1..d54b9295d 100644 --- a/labonneboite/common/env.py +++ b/labonneboite/common/env.py @@ -3,18 +3,18 @@ # Environment # ----------- -ENV_DEVELOPMENT = 'development' -ENV_TEST = 'test' -ENV_BONAPARTE = 'bonaparte' +ENV_DEVELOPMENT = "development" +ENV_TEST = "test" +ENV_BONAPARTE = "bonaparte" ENVS = [ENV_DEVELOPMENT, ENV_TEST, ENV_BONAPARTE] def get_current_env(): - current_env = os.getenv('LBB_ENV') + current_env = os.getenv("LBB_ENV") if current_env and current_env not in ENVS: raise Exception( "To identify the current environment, an `LBB_ENV` environment variable must be set " - "with one of those values: %s." % ', '.join(ENVS) + "with one of those values: %s." % ", ".join(ENVS) ) return current_env diff --git a/labonneboite/run.sh b/labonneboite/run.sh new file mode 100644 index 000000000..c66c0e7fc --- /dev/null +++ b/labonneboite/run.sh @@ -0,0 +1,3 @@ +# /bin/bash +alembic upgrade head +gunicorn --config python:wsgi-conf web.app:app \ No newline at end of file diff --git a/labonneboite/wsgi-conf.py b/labonneboite/wsgi-conf.py new file mode 100644 index 000000000..1419ad159 --- /dev/null +++ b/labonneboite/wsgi-conf.py @@ -0,0 +1,10 @@ +# Gunicorn configuration file +import multiprocessing + +bind = "0.0.0.0:8080" +workers = 2 +max_requests = 1000 +max_requests_jitter = 50 +log_file = "-" +reload = True +reload_engine = "poll" diff --git a/labonneboite/wsgi.py b/labonneboite/wsgi.py index 5eb970597..5349d31e5 100644 --- a/labonneboite/wsgi.py +++ b/labonneboite/wsgi.py @@ -1,10 +1 @@ from labonneboite.web import app -from labonneboite.common.env import ENV_DEVELOPMENT, get_current_env - -if __name__ == "__main__": - if get_current_env() == ENV_DEVELOPMENT: - # Since March 2020, PE Connect no longer allows redirect_uri with port 5000, however port 8080 works. - # Additionally 'localhost' works whereas '0.0.0.0' no longer does. - app.run(host="localhost", port=8080, debug=True) - else: - app.run(host="0.0.0.0") diff --git a/run.sh b/run.sh deleted file mode 100644 index 73585bca2..000000000 --- a/run.sh +++ /dev/null @@ -1,4 +0,0 @@ -# /bin/bash -alembic upgrade head - -gunicorn --workers 2 --bind 0.0.0.0:8080 web.app:app \ No newline at end of file From d8e2d121d79e648500d555f893ed61e3bfb8cf4a Mon Sep 17 00:00:00 2001 From: sylvaintouret Date: Tue, 20 Sep 2022 16:53:54 +0200 Subject: [PATCH 5/7] restructure to make ready for version 3.10 --- .gitignore | 1 + docker-compose.yml | 1 + docker/v3.10/Dockerfile | 42 +++ docker/v3.10/requirements.txt | 172 +++++++++++ docker/v3.10/setup.cfg | 94 ++++++ docker/v3.10/setup.py | 47 +++ Dockerfile => docker/v3.6/Dockerfile | 0 .../web/admin/views/office_admin_add.py | 287 +++++++++--------- labonneboite/web/contact_form/forms.py | 138 +++++---- 9 files changed, 586 insertions(+), 196 deletions(-) create mode 100644 docker/v3.10/Dockerfile create mode 100644 docker/v3.10/requirements.txt create mode 100644 docker/v3.10/setup.cfg create mode 100644 docker/v3.10/setup.py rename Dockerfile => docker/v3.6/Dockerfile (100%) diff --git a/.gitignore b/.gitignore index 311987ced..725eeece0 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,4 @@ labonneboite/web/static/gen/ # virtualenv venv/ logs/ +__pycache__ \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 3d8551930..8dd6ba4f4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,6 +27,7 @@ services: restart: always build: context: . + dockerfile: docker/v3.6/Dockerfile args: options: --reload environment: diff --git a/docker/v3.10/Dockerfile b/docker/v3.10/Dockerfile new file mode 100644 index 000000000..f635d3aac --- /dev/null +++ b/docker/v3.10/Dockerfile @@ -0,0 +1,42 @@ +FROM python:3.10.7-slim-bullseye + + +# Set timezone +ENV TZ=Europe/Paris +ENV FLASK_APP web.app + +# for mysql support & git & french langage support +RUN apt update && apt install -y \ + git \ + python3-dev \ + default-libmysqlclient-dev \ + build-essential \ + locales \ + --no-install-recommends + +# switch working directory +WORKDIR /app +RUN mkdir -p /app/logs + +# Installing requirements +# COPY docker/v3.6/requirements.txt /requirements.txt +# RUN pip install -r /requirements.txt + +# File imports : source code +COPY docker/v3.10/ /app/ +COPY ./labonneboite /app/labonneboite +RUN pip install -e . +WORKDIR /app/labonneboite + +# unsupported local error : https://stackoverflow.com/questions/54802935/docker-unsupported-locale-setting-when-running-python-container +RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \ + && sed -i -e 's/# fr_FR.UTF-8 UTF-8/fr_FR.UTF-8 UTF-8/' /etc/locale.gen \ + && locale-gen + +# building flask assets +RUN flask assets build + +# add the entrypoint +RUN chmod +x ./run.sh +RUN pip freeze +ENTRYPOINT ["/bin/bash", "./run.sh"] diff --git a/docker/v3.10/requirements.txt b/docker/v3.10/requirements.txt new file mode 100644 index 000000000..2dec63068 --- /dev/null +++ b/docker/v3.10/requirements.txt @@ -0,0 +1,172 @@ +alembic==1.8.1 +arabic-reshaper==2.1.3 +asn1crypto==1.5.1 +astroid==2.12.10 +asttokens==2.0.8 +async-generator==1.10 +attrs==22.1.0 +Babel==2.10.3 +backcall==0.2.0 +blinker==1.5 +build==0.8.0 +cachetools==5.2.0 +certifi==2022.9.14 +cffi==1.15.1 +chardet==5.0.0 +charset-normalizer==2.1.1 +click==8.1.3 +cryptography==38.0.1 +cssmin==0.2.0 +cssselect2==0.7.0 +decorator==5.1.1 +defusedxml==0.7.1 +dill==0.3.5.1 +dnspython==2.2.1 +EasyProcess==1.1 +ecdsa==0.18.0 +elastic-transport==8.4.0 +elasticsearch==8.4.1 +email-validator==1.3.0 +executing==1.0.0 +first==2.0.2 +Flask==2.2.2 +Flask-Admin==1.6.0 +Flask-Assets==2.0 +Flask-BabelEx==0.9.4 +Flask-Cors==3.0.10 +Flask-DebugToolbar==0.13.1 +Flask-Login==0.6.2 +Flask-Script==2.0.6 +Flask-Testing==0.8.1 +Flask-WTF==1.0.1 +future==0.18.2 +geographiclib==1.52 +geopy==2.2.0 +gevent==21.12.0 +google-api-core==2.10.1 +google-api-python-client==2.62.0 +google-auth==2.11.1 +google-auth-httplib2==0.1.0 +google-auth-oauthlib==0.5.3 +googleapis-common-protos==1.56.4 +greenlet==1.1.3 +gunicorn==20.1.0 +h11==0.13.0 +html5lib==1.1 +httplib2==0.20.4 +idna==3.4 +importlib-metadata==4.12.0 +ipdb==0.13.9 +ipython==8.5.0 +ipython-genutils==0.2.0 +isort==5.10.1 +itsdangerous==2.1.2 +jedi==0.18.1 +Jinja2==3.1.2 +jsmin==3.0.1 +# Editable install with no version control (LaBonneBoite==0.2) +-e /app +labonneboite-common @ git+https://github.com/StartupsPoleEmploi/labonneboite-common.git@f0dadcbb79338522169e586a9f4ec0a750920d00 +lazy-object-proxy==1.7.1 +line-profiler==3.5.1 +lxml==4.9.1 +mailjet-rest==1.3.4 +Mako==1.2.2 +MarkupSafe==2.1.1 +matplotlib-inline==0.1.6 +mccabe==0.7.0 +msgpack-python==0.5.6 +mysqlclient==2.1.1 +nose==1.3.7 +numpy==1.23.3 +oauthlib==3.2.1 +oscrypto==1.3.0 +outcome==1.2.0 +packaging==21.3 +pandas==1.5.0 +parameterized==0.8.1 +parso==0.8.3 +pep517==0.13.0 +pexpect==4.8.0 +pickleshare==0.7.5 +Pillow==9.2.0 +pip-tools==6.8.0 +platformdirs==2.5.2 +prompt-toolkit==3.0.31 +protobuf==4.21.6 +ptyprocess==0.7.0 +pure-eval==0.2.2 +pyasn1==0.4.8 +pyasn1-modules==0.2.8 +pycparser==2.21 +pycryptodomex==3.15.0 +Pygments==2.13.0 +pyHanko==0.14.0 +pyhanko-certvalidator==0.19.5 +pyjwkest==1.4.2 +PyJWT==2.5.0 +pylint==2.15.3 +pyparsing==3.0.9 +PyPDF2==2.10.9 +PyPDF3==1.0.6 +pyprof2calltree==1.4.5 +PySocks==1.7.1 +python-bidi==0.4.2 +python-dateutil==2.8.2 +python-editor==1.0.4 +python-jose==3.3.0 +python-slugify==6.1.2 +python3-openid==3.2.0 +pytz==2022.2.1 +pytz-deprecation-shim==0.1.0.post0 +PyVirtualDisplay==3.0 +PyYAML==6.0 +qrcode==7.3.1 +raven==6.10.0 +remote-pdb==2.1.0 +reportlab==3.6.11 +requests==2.28.1 +requests-oauthlib==1.3.1 +rsa==4.9 +selenium==4.4.3 +sentry-sdk==1.9.8 +six==1.16.0 +sniffio==1.3.0 +social-auth-app-flask==1.0.0 +social-auth-app-flask-sqlalchemy==1.0.1 +social-auth-core==4.3.0 +social-auth-storage-sqlalchemy==1.1.0 +sortedcontainers==2.4.0 +speaklater==1.3 +SQLAlchemy==1.3.24 +SQLAlchemy-Utils==0.38.3 +stack-data==0.5.0 +svglib==1.4.1 +text-unidecode==1.3 +tinycss2==1.1.1 +toml==0.10.2 +tomli==2.0.1 +tomlkit==0.11.4 +tqdm==4.64.1 +traitlets==5.4.0 +trio==0.21.0 +trio-websocket==0.9.2 +typing_extensions==4.3.0 +tzdata==2022.2 +tzlocal==4.2 +Unidecode==0.4.21 +uritemplate==4.1.1 +uritools==4.0.0 +urllib3==1.26.12 +validators==0.20.0 +wcwidth==0.2.5 +webassets==2.0 +webencodings==0.5.1 +Werkzeug==2.2.2 +wrapt==1.14.1 +wsproto==1.2.0 +WTForms==3.0.1 +xhtml2pdf==0.2.8 +zipp==3.8.1 +zope.event==4.5.0 +zope.interface==5.4.0 \ No newline at end of file diff --git a/docker/v3.10/setup.cfg b/docker/v3.10/setup.cfg new file mode 100644 index 000000000..2ed843971 --- /dev/null +++ b/docker/v3.10/setup.cfg @@ -0,0 +1,94 @@ +################################################## +# coverage # +################################################## +[coverage:run] +branch = True +parallel = true +concurrency=multiprocessing + +[coverage:report] +precision = 1 +show_missing = True +ignore_errors = True +exclude_lines = + pragma: no cover + raise NotImplementedError + def __repr__ + if settings.DEBUG + if __name__ == .__main__.: + if TYPE_CHECKING: +omit = + */test* + */migrations/* + manage.py + venv/* + +################################################## +# flake8 # +################################################## +[flake8] +ignore = + W503, # line break before binary operator + D100, # Missing docstring in public module + D101, # Missing docstring in public class + D102, # Missing docstring in public method + D104, # Missing docstring in public package + D106, # Missing docstring in public nested class + D200, # One-line docstring should fit on one line with quotes + D202, # No blank lines allowed after function docstring + D204, # 1 blank line required after class docstring + D205, # 1 blank line required between summary line and description + D400, # First line should end with a period + D406 # 1 blank line required before class docstring + +max-line-length = 120 +max-complexity = 18 +select = B,C,E,F,W,T4,B9 +enable-extensions = I,A,G,D +application-import-names = api,config,post,model_utils,saas,appointment,user +import-order-style = google +docstring-convention = numpy +exclude = migrations + +################################################## +# isort # +################################################## +[isort] +multi_line_output = 2 +line_length = 120 +order_by_type = false +force_to_top = labonneboite.conf + +################################################## +# mypy # +################################################## +[mypy] +ignore_missing_imports = false +follow_imports = silent +# no_strict_optional = true +show_error_codes = true +plugins = sqlmypy +disable_error_code = no-untyped-call +strict = true + +[mypy-alembic.*,astroid.*,flask_admin.*,dateutil.*,sqlalchemy_utils.*,babel.dates.*,easyprocess.*,elasticsearch.*,elasticsearch.exceptions.*,elasticsearch.helpers.*,flask_admin.*,flask_admin.contrib.sqla.*,flask_assets.*,flask_babelex.*,flask_debugtoolbar.*,flask.ext.cors.*,flask_login.*,flask_script.*,flask_testing.*,flask_wtf.*,flask_wtf.csrf.*,geopy.*,geopy.distance.*,googleapiclient.discovery.*,google_auth_oauthlib.flow.*,google.auth.transport.requests.*,ipdb.*,locust.*,mailjet_rest.*,MySQLdb.*,numpy.*,pandas.*,parameterized.*,pyprof2calltree.*,pyvirtualdisplay.*,sklearn.*,sklearn.metrics.*,social_core.*,social_core.backends.open_id_connect.*,social_core.exceptions.*,social_flask.routes.*,social_flask_sqlalchemy.models.*,social_flask.utils.*,unidecode.*,validators.*,wtforms.*,wtforms.fields.html5.*,wtforms.validators.*,wtforms.widgets.*,xhtml2pdf.*] +ignore_missing_imports = true + +################################################## +# yapf # +################################################## +[yapf] +based_on_style = google +column_limit = 120 +split_before_logical_operator = true +split_before_dot = true +coalesce_brackets = true +align_closing_bracket_with_visual_indent = true +allow_split_before_dict_value = false +blank_line_before_nested_class_or_def = true +blank_lines_around_top_level_definition = 2 + +[tool:pytest] +python_files = test_*.py +env = + LBB_ENV=test \ No newline at end of file diff --git a/docker/v3.10/setup.py b/docker/v3.10/setup.py new file mode 100644 index 000000000..022feb54a --- /dev/null +++ b/docker/v3.10/setup.py @@ -0,0 +1,47 @@ +import os +from setuptools import setup +import re + + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() + + +pip_editable_with_egg_regex = re.compile("-e (.+#egg=(.+))") + + +def requirement_to_install_require(requirement: str): + result = pip_editable_with_egg_regex.match(requirement) + if result: + return f"{result.group(2)} @ {result.group(1)}" + return requirement + + +install_requires = [ + requirement_to_install_require(req) for req in open("requirements.txt") +] + +setup( + name="LaBonneBoite", + version="0.2", + author="La Bonne Boite", + author_email="labonneboite@pole-emploi.fr", + description=(""), + packages=[ + "labonneboite", + ], + include_package_data=True, + install_requires=install_requires, + entry_points={ + "console_scripts": [ + "create_index = labonneboite.scripts.create_index:run", + ], + }, + classifiers=[ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: GNU Affero General Public License v3", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + ], +) diff --git a/Dockerfile b/docker/v3.6/Dockerfile similarity index 100% rename from Dockerfile rename to docker/v3.6/Dockerfile diff --git a/labonneboite/web/admin/views/office_admin_add.py b/labonneboite/web/admin/views/office_admin_add.py index 1122da42d..9cdeade8b 100644 --- a/labonneboite/web/admin/views/office_admin_add.py +++ b/labonneboite/web/admin/views/office_admin_add.py @@ -10,8 +10,18 @@ from labonneboite.common.scoring import get_hirings_from_score from labonneboite.conf import settings from labonneboite.web.admin.forms import code_commune_validator, zip_code_validator -from labonneboite.web.admin.forms import nospace_filter, phone_validator, strip_filter, siret_validator, naf_validator -from labonneboite.web.admin.utils import datetime_format, AdminModelViewMixin, SelectForChoiceTypeField +from labonneboite.web.admin.forms import ( + nospace_filter, + phone_validator, + strip_filter, + siret_validator, + naf_validator, +) +from labonneboite.web.admin.utils import ( + datetime_format, + AdminModelViewMixin, + SelectForChoiceTypeField, +) from labonneboite.scripts import create_index @@ -24,187 +34,190 @@ class OfficeAdminAddModelView(AdminModelViewMixin, ModelView): # type: ignore can_delete = False can_edit = False can_view_details = True - column_searchable_list = ['siret', 'company_name', 'office_name'] - column_default_sort = ('date_created', True) + column_searchable_list = ["siret", "company_name", "office_name"] + column_default_sort = ("date_created", True) page_size = 100 column_list = [ - 'siret', - 'company_name', - 'reason', - 'date_created', - 'date_updated', + "siret", + "company_name", + "reason", + "date_created", + "date_updated", ] column_details_list = [ - 'siret', - 'company_name', - 'office_name', - 'naf', - 'street_number', - 'street_name', - 'zipcode', - 'city_code', - 'email', - 'tel', - 'website', - 'flag_alternance', - 'flag_junior', - 'flag_senior', - 'flag_handicap', - 'departement', - 'headcount', - 'hiring', - 'score_alternance', - 'x', - 'y', - 'reason', - 'created_by', - 'date_created', - 'updated_by', - 'date_updated', + "siret", + "company_name", + "office_name", + "naf", + "street_number", + "street_name", + "zipcode", + "city_code", + "email", + "tel", + "website", + "flag_alternance", + "flag_junior", + "flag_senior", + "flag_handicap", + "departement", + "headcount", + "hiring", + "score_alternance", + "x", + "y", + "reason", + "created_by", + "date_created", + "updated_by", + "date_updated", ] column_formatters = { - 'date_created': datetime_format, - 'date_updated': datetime_format, + "date_created": datetime_format, + "date_updated": datetime_format, } column_labels = { - 'siret': "Siret", - 'company_name': "Raison sociale", - 'office_name': "Enseigne", - 'naf': "Code NAF", - 'street_number': "Numero rue", - 'street_name': "Libellé rue", - 'zipcode': "Code postal", - 'city_code': "Code commune", - 'email': "Email", - 'tel': "Téléphone", - 'website': "Site web", - 'flag_alternance': "Drapeau alternance", - 'flag_junior': "Drapeau junior", - 'flag_senior': "Drapeau senior", - 'flag_handicap': "Drapeau handicap", - 'departement': "Département", - 'headcount': "Tranche effectif", - 'hiring': "hiring", - 'score_alternance': "Score alternance", - 'x': "Longitude", - 'y': "Latitude", - 'reason': "Raison", - 'date_created': "Date de création", - 'date_updated': "Date de modification", - 'created_by': "Créé par", - 'updated_by': "Modifié par", + "siret": "Siret", + "company_name": "Raison sociale", + "office_name": "Enseigne", + "naf": "Code NAF", + "street_number": "Numero rue", + "street_name": "Libellé rue", + "zipcode": "Code postal", + "city_code": "Code commune", + "email": "Email", + "tel": "Téléphone", + "website": "Site web", + "flag_alternance": "Drapeau alternance", + "flag_junior": "Drapeau junior", + "flag_senior": "Drapeau senior", + "flag_handicap": "Drapeau handicap", + "departement": "Département", + "headcount": "Tranche effectif", + "hiring": "hiring", + "score_alternance": "Score alternance", + "x": "Longitude", + "y": "Latitude", + "reason": "Raison", + "date_created": "Date de création", + "date_updated": "Date de modification", + "created_by": "Créé par", + "updated_by": "Modifié par", } column_descriptions = { - 'reason': "Raison de l'ajout.", - 'hiring': "Valeur recommandée : entre " - f"{scoring.get_hirings_from_score(80)} et {scoring.get_hirings_from_score(90)}", - 'score_alternance': "Valeur recommandée : entre 80 et 90", + "reason": "Raison de l'ajout.", + "hiring": "Valeur recommandée : entre " + f"{scoring.get_hirings_from_score(80)} et {scoring.get_hirings_from_score(90)}", + "score_alternance": "Valeur recommandée : entre 80 et 90", } form_columns = [ - 'siret', - 'company_name', - 'office_name', - 'naf', - 'street_number', - 'street_name', - 'zipcode', - 'city_code', - 'departement', - 'email', - 'tel', - 'website', - 'flag_alternance', - 'flag_junior', - 'flag_senior', - 'flag_handicap', - 'headcount', - 'hiring', - 'score_alternance', - 'y', - 'x', - 'reason', + "siret", + "company_name", + "office_name", + "naf", + "street_number", + "street_name", + "zipcode", + "city_code", + "departement", + "email", + "tel", + "website", + "flag_alternance", + "flag_junior", + "flag_senior", + "flag_handicap", + "headcount", + "hiring", + "score_alternance", + "y", + "x", + "reason", ] form_overrides = { - 'headcount': SelectForChoiceTypeField, + "headcount": SelectForChoiceTypeField, } form_args = { - 'siret': { - 'filters': [strip_filter, nospace_filter], - 'validators': [DataRequired(), siret_validator], + "siret": { + "filters": [strip_filter, nospace_filter], + "validators": [DataRequired(), siret_validator], }, - - 'company_name': { - 'filters': [strip_filter], + "company_name": { + "filters": [strip_filter], }, - 'office_name': { - 'filters': [strip_filter], + "office_name": { + "filters": [strip_filter], }, - 'naf': { - 'filters': [strip_filter, nospace_filter], - 'validators': [naf_validator] + "naf": { + "filters": [strip_filter, nospace_filter], + "validators": [naf_validator], }, - 'street_number': { - 'filters': [strip_filter, nospace_filter], + "street_number": { + "filters": [strip_filter, nospace_filter], }, - 'street_name': { - 'filters': [strip_filter], + "street_name": { + "filters": [strip_filter], }, - 'zipcode': { - 'filters': [strip_filter, nospace_filter], - 'validators': [zip_code_validator], + "zipcode": { + "filters": [strip_filter, nospace_filter], + "validators": [zip_code_validator], }, - 'city_code': { - 'filters': [strip_filter, nospace_filter], - 'validators': [code_commune_validator], + "city_code": { + "filters": [strip_filter, nospace_filter], + "validators": [code_commune_validator], }, - 'departement': { - 'filters': [strip_filter, nospace_filter], + "departement": { + "filters": [strip_filter, nospace_filter], }, - 'email': { - 'validators': [validators.optional(), validators.Email()], + "email": { + "validators": [validators.optional(), validators.Email()], }, - 'tel': { - 'filters': [strip_filter, nospace_filter], - 'validators': [validators.optional(), phone_validator], + "tel": { + "filters": [strip_filter, nospace_filter], + "validators": [validators.optional(), phone_validator], }, - 'website': { - 'filters': [strip_filter], - 'validators': [validators.optional(), validators.URL()], + "website": { + "filters": [strip_filter], + "validators": [validators.optional(), validators.URL()], }, - 'headcount': { - 'choices': settings.HEADCOUNT_INSEE_CHOICES, + "headcount": { + "choices": settings.HEADCOUNT_INSEE_CHOICES, }, - 'hiring': { - 'validators': [validators.NumberRange(min=0, max=get_hirings_from_score(100))], + "hiring": { + "validators": [ + validators.NumberRange(min=0, max=get_hirings_from_score(100)) + ], }, - 'score_alternance': { - 'validators': [validators.NumberRange(min=0, max=100)], + "score_alternance": { + "validators": [validators.NumberRange(min=0, max=100)], }, - 'reason': { - 'filters': [strip_filter], + "reason": { + "filters": [strip_filter], }, - 'x': { - 'filters': [strip_filter, nospace_filter], - 'validators': [validators.required()], + "x": { + "filters": [strip_filter, nospace_filter], + "validators": [DataRequired()], # sytt: instead of [validators.required()] }, - 'y': { - 'filters': [strip_filter, nospace_filter], - 'validators': [validators.required()], + "y": { + "filters": [strip_filter, nospace_filter], + "validators": [DataRequired()], # sytt: instead of [validators.required()] }, } - def after_model_change(self, form: BaseForm, model: OfficeAdminAdd, is_created: bool) -> None: + def after_model_change( + self, form: BaseForm, model: OfficeAdminAdd, is_created: bool + ) -> None: """ Add new office in ElacticSearch and MySQL DB and remove it from OfficeAdminRemove if it exists in such DB """ - OfficeAdminRemove.query.filter_by(siret=form.data['siret']).delete() + OfficeAdminRemove.query.filter_by(siret=form.data["siret"]).delete() create_index.add_individual_office(model) diff --git a/labonneboite/web/contact_form/forms.py b/labonneboite/web/contact_form/forms.py index f3f42825d..9ac1d38a7 100644 --- a/labonneboite/web/contact_form/forms.py +++ b/labonneboite/web/contact_form/forms.py @@ -1,8 +1,17 @@ from flask import request from flask_wtf import FlaskForm -from wtforms import BooleanField, HiddenField, RadioField, SelectMultipleField, StringField, TextAreaField -from wtforms import validators +from wtforms import ( + BooleanField, + HiddenField, + RadioField, + SelectMultipleField, + StringField, + TextAreaField, +) +from wtforms import validators # sytt : this is import if it works, it's a fluke... + +# from wtforms.fields import EmailField, TelField # compatibility 3.10 : wtfform > 3.0.0 from wtforms.fields.html5 import EmailField, TelField from wtforms.validators import DataRequired, Email, Optional, Regexp, URL from wtforms.widgets import ListWidget, CheckboxInput @@ -12,7 +21,7 @@ PHONE_REGEX = r"^(0|\+33)[1-9]([-. ]?[0-9]{2}){4}$" -SIRET_REGEX = r'[0-9]{14}' +SIRET_REGEX = r"[0-9]{14}" class MultiCheckboxField(SelectMultipleField): @@ -24,6 +33,7 @@ class MultiCheckboxField(SelectMultipleField): https://wtforms.readthedocs.io/en/stable/specific_problems.html#specialty-field-tricks """ + widget = ListWidget(prefix_label=False) option_widget = CheckboxInput() @@ -31,53 +41,55 @@ class MultiCheckboxField(SelectMultipleField): class OfficeIdentificationForm(FlaskForm): siret = StringField( - 'N° de Siret *', + "N° de Siret *", validators=[ DataRequired(), - Regexp(SIRET_REGEX, message=("Le siret de l'établissement est invalide (14 chiffres)")) + Regexp( + SIRET_REGEX, + message=("Le siret de l'établissement est invalide (14 chiffres)"), + ), ], - description= - "14 chiffres, sans espace. Exemple: 36252187900034
" - 'Retrouver mon numéro de siret ', + description="14 chiffres, sans espace. Exemple: 36252187900034
" + 'Retrouver mon numéro de siret ', ) - last_name = StringField('Nom *', validators=[DataRequired()]) - first_name = StringField('Prénom *', validators=[DataRequired()]) + last_name = StringField("Nom *", validators=[DataRequired()]) + first_name = StringField("Prénom *", validators=[DataRequired()]) phone = TelField( - 'Téléphone *', + "Téléphone *", validators=[DataRequired(), Regexp(PHONE_REGEX)], - render_kw={"placeholder": "01 77 86 39 49, +331 77 86 39 49"} + render_kw={"placeholder": "01 77 86 39 49, +331 77 86 39 49"}, ) email = EmailField( - 'Adresse email *', + "Adresse email *", validators=[DataRequired(), Email()], - render_kw={"placeholder": "exemple@domaine.com"} + render_kw={"placeholder": "exemple@domaine.com"}, ) class OfficeHiddenIdentificationForm(FlaskForm): - siret = HiddenField('Siret *', validators=[DataRequired()]) - last_name = HiddenField('Nom *', validators=[DataRequired()]) - first_name = HiddenField('Prénom *', validators=[DataRequired()]) + siret = HiddenField("Siret *", validators=[DataRequired()]) + last_name = HiddenField("Nom *", validators=[DataRequired()]) + first_name = HiddenField("Prénom *", validators=[DataRequired()]) phone = HiddenField( - 'Téléphone *', + "Téléphone *", validators=[DataRequired(), Regexp(PHONE_REGEX)], - render_kw={"placeholder": "01 77 86 39 49, +331 77 86 39 49"} + render_kw={"placeholder": "01 77 86 39 49, +331 77 86 39 49"}, ) email = HiddenField( - 'Adresse email *', + "Adresse email *", validators=[DataRequired(), Email()], - render_kw={"placeholder": "exemple@domaine.com"} + render_kw={"placeholder": "exemple@domaine.com"}, ) class OfficeOtherRequestForm(OfficeHiddenIdentificationForm): comment = TextAreaField( - 'Votre message*', + "Votre message*", validators=[DataRequired(), validators.length(max=15000)], - description="15000 caractères maximum" + description="15000 caractères maximum", ) @@ -103,8 +115,10 @@ def __init__(self, *args, **kwargs): associated with the current office. """ super().__init__(*args, **kwargs) - self.office = kwargs.pop('office') - self.romes_choices = [(rome.code, rome.name) for rome in self.office.romes_for_naf_mapping] + self.office = kwargs.pop("office") + self.romes_choices = [ + (rome.code, rome.name) for rome in self.office.romes_for_naf_mapping + ] self.romes_to_keep.choices = self.romes_choices self.romes_alternance_to_keep.choices = self.romes_choices @@ -123,28 +137,34 @@ def validate(self): # `extra_romes_to_add` and `extra_romes_alternance_to_add` are pupulated via JavaScript. # Those fields are defined outside of the form class so we use `request.form` to get them. extra_romes_to_add = [ - rome for rome in request.form.getlist('extra_romes_to_add') - if rome in settings.ROME_DESCRIPTIONS and rome not in self.office.romes_codes + rome + for rome in request.form.getlist("extra_romes_to_add") + if rome in settings.ROME_DESCRIPTIONS + and rome not in self.office.romes_codes ] extra_romes_alternance_to_add = [ - rome for rome in request.form.getlist('extra_romes_alternance_to_add') - if rome in settings.ROME_DESCRIPTIONS and rome not in self.office.romes_codes + rome + for rome in request.form.getlist("extra_romes_alternance_to_add") + if rome in settings.ROME_DESCRIPTIONS + and rome not in self.office.romes_codes ] # Checked ROME codes. romes_to_keep = self.romes_to_keep.data or [] romes_to_add = set(romes_to_keep + extra_romes_to_add) romes_alternance_to_keep = self.romes_alternance_to_keep.data or [] - romes_alternance_to_add = set(romes_alternance_to_keep + extra_romes_alternance_to_add) + romes_alternance_to_add = set( + romes_alternance_to_keep + extra_romes_alternance_to_add + ) # Unchecked ROME codes. romes_to_remove = self.office.romes_codes - romes_to_add romes_alternance_to_remove = self.office.romes_codes - romes_alternance_to_add - setattr(self, 'romes_to_add', romes_to_add) - setattr(self, 'romes_alternance_to_add', romes_alternance_to_add) - setattr(self, 'romes_to_remove', romes_to_remove) - setattr(self, 'romes_alternance_to_remove', romes_alternance_to_remove) + setattr(self, "romes_to_add", romes_to_add) + setattr(self, "romes_alternance_to_add", romes_alternance_to_add) + setattr(self, "romes_to_remove", romes_to_remove) + setattr(self, "romes_alternance_to_remove", romes_alternance_to_remove) return True @@ -152,57 +172,57 @@ def validate(self): class OfficeUpdateCoordinatesForm(OfficeHiddenIdentificationForm): CONTACT_MODES = ( - ('mail', 'Par courrier'), - ('email', 'Par email'), - ('phone', 'Par téléphone'), - ('office', 'Sur place'), - ('website', 'Via votre site internet'), + ("mail", "Par courrier"), + ("email", "Par email"), + ("phone", "Par téléphone"), + ("office", "Sur place"), + ("website", "Via votre site internet"), ) CONTACT_MODES_LABELS = dict(CONTACT_MODES) # Note : we add new_ to avoid conflict with request.args new_contact_mode = RadioField( - 'Mode de contact à privilégier', - choices=CONTACT_MODES, - default='email' + "Mode de contact à privilégier", choices=CONTACT_MODES, default="email" ) new_website = StringField( - 'Site Internet', - validators=[URL(), Optional()], render_kw={"placeholder": "http://exemple.com"} + "Site Internet", + validators=[URL(), Optional()], + render_kw={"placeholder": "http://exemple.com"}, ) new_email = EmailField( - 'Email recruteur', - validators=[Email(), Optional()], render_kw={"placeholder": "exemple@domaine.com"} + "Email recruteur", + validators=[Email(), Optional()], + render_kw={"placeholder": "exemple@domaine.com"}, ) new_phone = StringField( - 'Téléphone', + "Téléphone", validators=[Optional(), Regexp(PHONE_REGEX)], - render_kw={"placeholder": "01 77 86 39 49, +331 77 86 39 49"} + render_kw={"placeholder": "01 77 86 39 49, +331 77 86 39 49"}, ) - social_network = StringField('Réseau social', validators=[URL(), Optional()]) + social_network = StringField("Réseau social", validators=[URL(), Optional()]) new_email_alternance = EmailField( - 'Email recruteur spécialisé alternance', + "Email recruteur spécialisé alternance", validators=[validators.optional(), Email()], - render_kw={"placeholder": "exemple-alternance@domaine.com"} + render_kw={"placeholder": "exemple-alternance@domaine.com"}, ) new_phone_alternance = StringField( - 'Téléphone du recruteur spécialisé alternance', + "Téléphone du recruteur spécialisé alternance", validators=[validators.optional(), Regexp(PHONE_REGEX)], - render_kw={"placeholder": "01 77 86 39 49, +331 77 86 39 49"} + render_kw={"placeholder": "01 77 86 39 49, +331 77 86 39 49"}, ) rgpd_consent = BooleanField( - 'En cochant cette case, vous consentez à diffuser des données à caractère personnel sur les services numériques de Pôle emploi.', - [validators.required()] + "En cochant cette case, vous consentez à diffuser des données à caractère personnel sur les services numériques de Pôle emploi.", + validators=[DataRequired()], # sytt: instead of [validators.required()] ) class OfficeRemoveForm(OfficeHiddenIdentificationForm): remove_lbb = BooleanField( - 'Supprimer mon entreprise du service La Bonne Boite puisque je ne suis pas intéressé-e pour recevoir des candidatures spontanées via ce site', - [validators.optional()] + "Supprimer mon entreprise du service La Bonne Boite puisque je ne suis pas intéressé-e pour recevoir des candidatures spontanées via ce site", + [validators.optional()], ) remove_lba = BooleanField( - 'Supprimer mon entreprise du service La Bonne Alternance puisque je ne suis pas intéressé-e pour recevoir des candidatures spontanées via ce site', - [validators.optional()] + "Supprimer mon entreprise du service La Bonne Alternance puisque je ne suis pas intéressé-e pour recevoir des candidatures spontanées via ce site", + [validators.optional()], ) From 27711caa8c38769d7082ebd23c43412b270d9ce4 Mon Sep 17 00:00:00 2001 From: sylvaintouret Date: Tue, 20 Sep 2022 17:02:22 +0200 Subject: [PATCH 6/7] health check on docker-compose does not work properly --- docker-compose.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8dd6ba4f4..ab4b80742 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,8 +36,10 @@ services: - ./logs:/app/labonneboite/logs - ./labonneboite:/app/labonneboite depends_on: - - elasticsearch - - mysql + elasticsearch: + condition: service_started + mysql: + condition: service_started ports: - 8080:8080 @@ -72,11 +74,11 @@ services: <<: *dbenv ports: - 3306:3306 - # healthcheck: - # test: mysql ${MYSQL_DATABASE} --user=${MYSQL_USER} --password='${MYSQL_PASSWORD}' --silent --execute "SELECT 1;" - # interval: 10s - # retries: 5 - # timeout: 2s + healthcheck: + test: mysql ${MYSQL_DATABASE} --user=${MYSQL_USER} --password='${MYSQL_PASSWORD}' --silent --execute "SELECT 1;" + interval: 10s + retries: 5 + timeout: 2s # Dev tools adminer: From c72120cf177b5e5461db0bcdc881c355978c0a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9ni=20Marvaud?= <24732919+lmarvaud@users.noreply.github.com> Date: Fri, 23 Sep 2022 09:53:37 +0200 Subject: [PATCH 7/7] :recycle: upgrade python to v3.10 --- .github/workflows/lbb-ci.yml | 9 +- Makefile | 32 +- README.md | 14 +- labonneboite/common/load_data.py | 1 - labonneboite/common/scoring.py | 5 + labonneboite/tests/importer/__init__.py | 0 ...tablissement_backup_2020_11_12_1746.sql.gz | Bin 1087 -> 0 bytes ...tablissement_backup_2019_11_10_1716.sql.gz | Bin 1257 -> 0 bytes .../lbb_etablissement_full_201612192300.csv | 27 - .../data/lbb_xdpdpae_delta_201611102200.csv | 7 - .../lbb_xdpdpae_delta_201611102200.csv.bz2 | Bin 443 -> 0 bytes .../lbb_xdpdpae_delta_201611102200.csv.gz | Bin 410 -> 0 bytes .../data/lbb_xdpdpae_delta_201612102200.csv | 7 - .../importer/data/perf_division_per_rome.csv | 11 - .../data/perf_importer_cycle_infos.csv | 4 - .../data/perf_prediction_and_effective_h.csv | 17 - labonneboite/tests/importer/test_base.py | 33 - .../tests/importer/test_compute_score.py | 54 -- labonneboite/tests/importer/test_dpae.py | 65 -- .../tests/importer/test_etablissements.py | 114 --- labonneboite/tests/importer/test_geocode.py | 73 -- .../tests/importer/test_perf_compute_data.py | 134 --- .../tests/importer/test_perf_insert_data.py | 44 - labonneboite/tests/importer/test_scoring.py | 38 - .../tests/scripts/test_create_index.py | 34 +- labonneboite/tests/selenium/base.py | 5 +- .../test_make_a_new_search_on_search_page.py | 21 +- labonneboite/tests/selenium/test_reset_naf.py | 7 +- labonneboite/tests/selenium/test_results.py | 12 +- labonneboite/tests/selenium/test_simple.py | 8 +- labonneboite/tests/test_base.py | 39 +- labonneboite/tests/web/front/test_admin.py | 18 +- labonneboite/tests/web/front/test_auth.py | 22 +- .../tests/web/front/test_contact_form.py | 249 ++--- .../tests/web/front/test_favorites.py | 67 +- .../tests/web/front/test_pro_version.py | 85 +- labonneboite/tests/web/front/test_root.py | 30 +- labonneboite/tests/web/front/test_routes.py | 8 +- .../tests/web/front/test_user_account.py | 21 +- .../web/admin/views/office_admin_add.py | 4 +- labonneboite/web/api/util.py | 3 +- labonneboite/web/contact_form/forms.py | 21 +- labonneboite/web/office/views.py | 2 +- labonneboite/web/templates_functions.py | 2 +- labonneboite/web/user/views.py | 4 +- requirements.dev.in | 1 + requirements.dev.txt | 874 +++++++++++++++--- requirements.in | 14 +- requirements.txt | 591 +++++++++--- 49 files changed, 1596 insertions(+), 1235 deletions(-) delete mode 100644 labonneboite/tests/importer/__init__.py delete mode 100644 labonneboite/tests/importer/data/dummy_export_etablissement_backup_2020_11_12_1746.sql.gz delete mode 100644 labonneboite/tests/importer/data/export_etablissement_backup_2019_11_10_1716.sql.gz delete mode 100644 labonneboite/tests/importer/data/lbb_etablissement_full_201612192300.csv delete mode 100644 labonneboite/tests/importer/data/lbb_xdpdpae_delta_201611102200.csv delete mode 100644 labonneboite/tests/importer/data/lbb_xdpdpae_delta_201611102200.csv.bz2 delete mode 100644 labonneboite/tests/importer/data/lbb_xdpdpae_delta_201611102200.csv.gz delete mode 100644 labonneboite/tests/importer/data/lbb_xdpdpae_delta_201612102200.csv delete mode 100644 labonneboite/tests/importer/data/perf_division_per_rome.csv delete mode 100644 labonneboite/tests/importer/data/perf_importer_cycle_infos.csv delete mode 100644 labonneboite/tests/importer/data/perf_prediction_and_effective_h.csv delete mode 100644 labonneboite/tests/importer/test_base.py delete mode 100644 labonneboite/tests/importer/test_compute_score.py delete mode 100644 labonneboite/tests/importer/test_dpae.py delete mode 100644 labonneboite/tests/importer/test_etablissements.py delete mode 100644 labonneboite/tests/importer/test_geocode.py delete mode 100644 labonneboite/tests/importer/test_perf_compute_data.py delete mode 100644 labonneboite/tests/importer/test_perf_insert_data.py delete mode 100644 labonneboite/tests/importer/test_scoring.py diff --git a/.github/workflows/lbb-ci.yml b/.github/workflows/lbb-ci.yml index b3a5876fd..6fafd9400 100644 --- a/.github/workflows/lbb-ci.yml +++ b/.github/workflows/lbb-ci.yml @@ -4,9 +4,14 @@ name: LBB CI on: push: - branches: [ "master"] + branches: [ "*"] pull_request: branches: [ "master"] + workflow_dispatch: + inputs: + git-ref: + description: Git Ref (Optional) + required: false jobs: tests: @@ -15,7 +20,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.6.8] + python-version: [3.10.4] steps: - uses: actions/checkout@v3 diff --git a/Makefile b/Makefile index 11ede7272..d466f094d 100644 --- a/Makefile +++ b/Makefile @@ -215,53 +215,53 @@ start-locust-against-localhost: # Tests # ----- -NOSETESTS_OPTS ?= -NOSETESTS = nosetests -s $(NOSETESTS_OPTS) +PYTEST_OPTS ?= -vx +PYTEST = pytest $(PYTEST_OPTS) TESTS = labonneboite/tests/app/ \ labonneboite/tests/web/ \ labonneboite/tests/scripts/ test-unit: clean-pyc rebuild-data-test - LBB_ENV=test $(NOSETESTS) ${TESTS} + LBB_ENV=test $(PYTEST) ${TESTS} test: test-unit test-selenium test-integration check-all: test-all test-app: - LBB_ENV=test $(NOSETESTS) labonneboite/tests/app + LBB_ENV=test $(PYTEST) labonneboite/tests/app test-api: - LBB_ENV=test $(NOSETESTS) labonneboite/tests/web/api + LBB_ENV=test $(PYTEST) labonneboite/tests/web/api test-front: - LBB_ENV=test $(NOSETESTS) labonneboite/tests/web/front + LBB_ENV=test $(PYTEST) labonneboite/tests/web/front test-web: test-api test-front test-web-integration test-scripts: - LBB_ENV=test $(NOSETESTS) labonneboite/tests/scripts + LBB_ENV=test $(PYTEST) labonneboite/tests/scripts test-integration: clear-data-test database-test populate-data-test - LBB_ENV=test $(NOSETESTS) labonneboite/tests/integration + LBB_ENV=test $(PYTEST) labonneboite/tests/integration test-selenium: clear-data-test database-test populate-data-test-selenium - LBB_ENV=test SELENIUM_IS_SETUP=1 $(NOSETESTS) labonneboite/tests/selenium + LBB_ENV=test SELENIUM_IS_SETUP=1 $(PYTEST) labonneboite/tests/selenium # Convenient reminder about how to run a specific test manually. test-custom: @echo "To run a specific test, run for example:" @echo - @echo " $$ LBB_ENV=test nosetests -s labonneboite/tests/web/api/test_api.py" + @echo " $$ LBB_ENV=test pytest -s labonneboite/tests/web/api/test_api.py" @echo @echo "and you can even run a specific method, here are several examples:" @echo - @echo " $$ LBB_ENV=test nosetests -s labonneboite/tests/web/api/test_api.py:ApiCompanyListTest.test_query_returns_scores_adjusted_to_rome_code_context" - @echo " $$ LBB_ENV=test nosetests -s labonneboite/tests/web/api/test_api.py:ApiOffersOfficesListTest" - @echo " $$ LBB_ENV=test nosetests -s labonneboite/tests/web/front/test_routes.py" - @echo " $$ LBB_ENV=test nosetests -s labonneboite/tests/app/test_suggest_locations.py" - @echo " $$ LBB_ENV=test nosetests -s labonneboite/tests/scripts/test_create_index.py:DeleteOfficeAdminTest.test_office_admin_add" - @echo " $$ LBB_ENV=test nosetests -s labonneboite/tests/selenium/test_search_selecting_car.py:TestSearchSelectingCar.test_commute_time_is_displayed" + @echo " $$ LBB_ENV=test pytest -s labonneboite/tests/web/api/test_api.py:ApiCompanyListTest.test_query_returns_scores_adjusted_to_rome_code_context" + @echo " $$ LBB_ENV=test pytest -s labonneboite/tests/web/api/test_api.py:ApiOffersOfficesListTest" + @echo " $$ LBB_ENV=test pytest -s labonneboite/tests/web/front/test_routes.py" + @echo " $$ LBB_ENV=test pytest -s labonneboite/tests/app/test_suggest_locations.py" + @echo " $$ LBB_ENV=test pytest -s labonneboite/tests/scripts/test_create_index.py:DeleteOfficeAdminTest.test_office_admin_add" + @echo " $$ LBB_ENV=test pytest -s labonneboite/tests/selenium/test_search_selecting_car.py:TestSearchSelectingCar.test_commute_time_is_displayed" @echo @echo "Note that you can set the env var `NOSE_NOCAPTURE=1` to keep logs in the console" diff --git a/README.md b/README.md index e0dcd1049..37dad82cc 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ La Bonne Boite is a [web site](https://labonneboite.pole-emploi.fr) and an [API] ### Install OS requirements: -- python == 3.6.8 +- python == 3.10.4 - docker-compose - mysql-client - libmysqlclient-dev @@ -108,19 +108,19 @@ On fedora You will also need to install docker and docker-compose. Follow the instructions related to your particular OS from the [official Docker documentation](https://docs.docker.com/install/). -### Create a virtualenv for Python 3.6 +### Create a virtualenv for Python 3.10 -For now, La Bonne Boite runs in production under Python 3.6.8. You are going to have to create a virtualenv that runs this specific version of Python. +For now, La Bonne Boite runs in production under Python 3.10.4. You are going to have to create a virtualenv that runs this specific version of Python. - $ wget https://www.python.org/ftp/python/3.6.8/Python-3.6.8.tgz - $ tar -xvzf Python-3.6.8.tgz - $ cd Python-3.6.8 + $ wget https://www.python.org/ftp/python/3.10.4/Python-3.10.4.tgz + $ tar -xvzf Python-3.10.4.tgz + $ cd Python-3.10.4 $ ./configure --prefix=/usr/local --enable-loadable-sqlite-extensions $ sudo make altinstall Create an [isolated Python environment](https://virtualenv.pypa.io/), for example using [virtualenvwrapper](https://virtualenvwrapper.readthedocs.io/): - $ mkvirtualenv --python=`which python3.6` lbb + $ mkvirtualenv --python=`which python3.10` lbb $ workon lbb You might need to add `labonneboite` base directory to the Python path. This has to be run only once. One way to do it using `virtualenvwrapper`: diff --git a/labonneboite/common/load_data.py b/labonneboite/common/load_data.py index a45780918..a29634483 100644 --- a/labonneboite/common/load_data.py +++ b/labonneboite/common/load_data.py @@ -1,7 +1,6 @@ import os import pickle import csv -import pandas as pd import math from functools import lru_cache, reduce diff --git a/labonneboite/common/scoring.py b/labonneboite/common/scoring.py index beca01946..0fb327460 100644 --- a/labonneboite/common/scoring.py +++ b/labonneboite/common/scoring.py @@ -1,6 +1,7 @@ import math from functools import lru_cache from typing import Optional, Union +from decimal import Decimal from labonneboite.common import mapping as mapping_util from labonneboite.common.conf import settings @@ -124,6 +125,10 @@ def get_hirings_from_score(score: Score) -> Hiring: """ does exactly the reverse operation of get_score_from_hirings """ + + if (isinstance(score, Decimal)): + score = int(score) + if score <= 50: hirings = settings.SCORE_50_HIRINGS * score / 50.0 elif score <= 60: diff --git a/labonneboite/tests/importer/__init__.py b/labonneboite/tests/importer/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/labonneboite/tests/importer/data/dummy_export_etablissement_backup_2020_11_12_1746.sql.gz b/labonneboite/tests/importer/data/dummy_export_etablissement_backup_2020_11_12_1746.sql.gz deleted file mode 100644 index 78e980fad44a9f5362e197f83408f34e0367b463..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1087 zcmV-F1i zZ=*OAhVS=RSX`slHrT-4xrrxbMIbU{)tQ@!NomV&fCz}X^Y6zt(3X$Rw9>Xo8#~AN z@q5mBZ8UkEwT5ZbY2H+2Qyug4l*w^cHd+2ys*lI&J}c7)`Khhb^7xcx4-eU~&DBhK zDYCk%FDmYof6q>*tQ)9B=c;MblUkRlzgdyy>i={0y~$e@^l(Za59z7R>QX-VW#*qt znOF5NYO=SZr?kA!kLg*CpVH<~TuyC%c+9F(bxhk_o}l0RSQW*k$FJ)9yiD7yIs7*n z|Bz$JnbvJqWTn!fIaYNxz>eAUSCc8N*P+bX=c@i!PMn{rauU-YcRYEl$~HZ=heDzb zM>|(C=;8cw)N|1kXX6h8B8myLEek@7p<%9W**=c$o)G|zZg&cxi z)Ap_qave2qm>yOS!j`Fipa~VDhbi=IEH^^bFhhS81uOTKG^U` zY%JPAZX&x>O?6fng11;|Upp#8zZvAp0U%=(8l@a~=W*pe_5x zgJ1z=K_l1hHA1%4YqXqL1YK95SObd8o7@Dj{T*O}hf5M+!}Pa17RTeT_bLu80ZOg9 z97=ICAF^}~;f&9tqa|!o z-GUtpm>+|++*$Z>!h&hTS{>JhZEtvzuupgBv=J=Sdm8jQT1(4`X4xgg8&{uHrOTc4 z){u$y%v$;c%d-Sb9w+keK3jRR;uRe9oF2}0RkSl@qsSR4`NH<kB_hz%37jKlc#jH7be9+oHXA-`Re5 z*P~hKAQbN|CBMOH_INS{*N-3_m1{1ZK;7pYkb F000tK9Vh?* diff --git a/labonneboite/tests/importer/data/export_etablissement_backup_2019_11_10_1716.sql.gz b/labonneboite/tests/importer/data/export_etablissement_backup_2019_11_10_1716.sql.gz deleted file mode 100644 index c3a29dce63398b35c2160595428f259fe3bc724a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1257 zcmVF3D zGh`85y3I@;X`f?XY^za6379caQL*}~kFM_O`f0ae^40xd!hF440Z}@n2QZGf$f6ls zb_ShZw*`wd12an2PtbVMRA22kYwb2(@HXY*3P^X*QSeOwU*kXovw*WJh=W<0By5&O zf=O`ur(ROiW!V>&0e*OtCcj*E)J{*W)h=HB+E<7|z@;AC*yh+Dz11CkcUzqw8OF9 zp4~Gh)c<5#6e*wY%h)}v{+ps)DN22hQAmH!H?@opQ;LSu(K~5r;eQK?!^#noTic<= z8_R!3Kkk{z!WL8jl_SfDUg=Nlv9xn{+k$6d-Lids>e>Dux{33{RsBRA`@l#-zTvWL zkGfm3{C>l>5pO23b!#t}#SHKatV6NRSnmHKm)$Y>8*$E-ED_vK*yEj4s*N4{J$U+S zlY(&z^e@9j_ozaDYbg)Du(=;{Ka3*;$s+{UaB~wr1X&OYmie5C-C${xrv0r!{dSVaCkOXD;Q1ZFsX94}1_)8%q~=0Qob zd6Wc#asTNK7I7Mg@1wpRMWJ^EnaD%R&pQ|o(+myDEk@O97b@-`dlDzdsGPo_zI##; zmIpD6i{dfO{@!fD3(v{*Ru5^yO8#|<=GL`;N;gasfwA9TqU?07+%H!tK60^o$$eK# zzC7-`?;T^JJ0Ag4IW)E@q$vla;Lb1jMWf&*2dHgqXl#Q-v(^M^y)m$CF_I)5zkYTF zS%6{2yWX0$GxNG^=4mOilz0#F{}^lGD7l9@*0QH0f72el+vM2loDY9GhDUdGlcIob zVkP;#tUNZ{d=M9DQ)L>KOC1jk%d@LB@-oPUMqTM?nlezj@?hlwcc+``mhC@+I`U5OCTlL+gJil?Md#Tl1ZEO}awWpF+U+ZW?MU^8GSl@m< zU!+Bvtw#NlkiOa*3{`2=07uNu>&^&t*R@9mbvI~L?RG!y=GRlU6V#_~rfzOLbuDU2 z+)&Hsr;u*1f2j_%;YR;Kh}G4_#pB~+C!U1?m*If%&LS(Apboo3)b4kN>g7;t^|47J z8j7;LO(#)mF3RO= zb-#A3lY2@3xm!Eh$W<^@_G-tvxN>)K&{!1uj9oa%V+o$4u%1C4& z4YsRLF@?hxOnl2wGfkeB{TBzPnOyv>jgj+-D-pP0X&0B=_E&-u9^$S7mn>0|MF*t` zTuf|x8s1BZsYMkNjrkHYk=mMU%qpk@fJoT>&a;)UHwgHbpIj z-~+G>v?Son!RCOJIMRS;n;f#S?{$zbfsT>MJn4BMiMOV<@2>RQE2$ie<+?4M)pZV8 zOiLg~W=60&PeiQR5p+pEm16S&d1h*N(xt1*w^((tfM+$QA{LLyDFGs5e?Az_b6JM5 l_!*qyNa-z<=#daH_4S#cKYt{+WVk(r+>uTcBo^*McEF<{$IJi# diff --git a/labonneboite/tests/importer/data/lbb_xdpdpae_delta_201611102200.csv.gz b/labonneboite/tests/importer/data/lbb_xdpdpae_delta_201611102200.csv.gz deleted file mode 100644 index e6363340cfbcb79087203d5a7dfb7f62bd62a8fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 410 zcmV;L0cHLliwFqIJbGOK18ibqUwCA2WN=|+Uu0!$bYWjIFfleUF)=VPFfuYQFfLV;=5QX+1nHcj~(~y!jza zZE6@pv+|;CEt&LzUe%cQlwY8Db_lOTD=0~M6rs^ZQ$S>hyeZ#i+0nO&C6>AuIAat?C7tL4noc$C12_+$cV%`G) E03f8h`v3p{ diff --git a/labonneboite/tests/importer/data/lbb_xdpdpae_delta_201612102200.csv b/labonneboite/tests/importer/data/lbb_xdpdpae_delta_201612102200.csv deleted file mode 100644 index 510d408e5..000000000 --- a/labonneboite/tests/importer/data/lbb_xdpdpae_delta_201612102200.csv +++ /dev/null @@ -1,7 +0,0 @@ -kc_siret|dc_naf_id|dc_adresse|dc_codepostal|dc_commune_id|dn_tailleetablissement|dc_communenaissancepays|kd_dateembauche|dc_typecontrat_id|dd_datefincdd|dc_romev3_1_id|dc_romev3_2_id|kd_datecreation|dc_privepublic|dc_commune_id|dc_natureemploi_id|dc_qualitesalarie_id|dn_dureetravailhebdo|dn_dureetravailmensuelle|dn_dureetravailannuelle|nbrjourtravaille|iiann|dc_lblprioritede|kn_trancheage|duree_pec|dc_ididentiteexterne|premiere_embauche -51146379600017|||69800||||2016-10-01 00:00:00|1||||||||||||200||NULL|de 26 ans ? 50 ans|10|| -51146379600017|||14700||||2016-11-15 00:00:00|2||||||||||||3||NULL|- de 26 ans|30|| -51146379600017|||14700||||2016-11-14 00:00:00|2||||||||||||3||NULL|+ de 50 ans|30|| -51146379600017|||14700||||2021-11-07 00:00:00|1||||||||||||300||NULL|+ de 50 ans|30|| -51146379600017|||14700||||2016-10-01 00:00:00|2||||||||||||3||NULL|+ de 50 ans|30|| -03880702000011|||14700||||2015-04-09 00:00:00|2||||||||||||3||NULL|+ de 50 ans|40|| diff --git a/labonneboite/tests/importer/data/perf_division_per_rome.csv b/labonneboite/tests/importer/data/perf_division_per_rome.csv deleted file mode 100644 index 01effc71a..000000000 --- a/labonneboite/tests/importer/data/perf_division_per_rome.csv +++ /dev/null @@ -1,11 +0,0 @@ -id;ici_id;rome;naf;threshold_lbb;nb_bonnes_boites_lbb;threshold_lba;nb_bonnes_boites_lba -1;1;A1205;8130Z;7;500;7;500 -2;2;A1205;8130Z;7;0;7;0 -3;3;M1203;;20;500;7;0 -4;1;M1203;1091Z;20;0;7;0 -5;2;N4403;4920Z;12;75;7;0 -6;3;A1205;8130Z;12;500;7;0 -7;2;M1203;;17;120;7;0 -8;2;M1203;1091Z;19;500;7;0 -9;2;N4403;4920Z;5;0;7;0 -10;2;A1205;8130Z;5;500;7;0 \ No newline at end of file diff --git a/labonneboite/tests/importer/data/perf_importer_cycle_infos.csv b/labonneboite/tests/importer/data/perf_importer_cycle_infos.csv deleted file mode 100644 index 35fa113ca..000000000 --- a/labonneboite/tests/importer/data/perf_importer_cycle_infos.csv +++ /dev/null @@ -1,4 +0,0 @@ -id;execution_date;prediction_start_date;prediction_end_date;file_name;computed;on_google_sheets -1;2020-08-18 08:15:27.243860;2020-09-01 08:15:27.243860;2021-02-28 08:15:27.243860;export_etablissement_backup_2020_08_18_1143.sql.gz;True;False -2;2020-01-18 08:15:27.243860;2020-02-01 08:15:27.243860;2020-07-31 08:15:27.243860;export_etablissement_backup_2020_08_18_114.sql.gz;True;False -3;2019-12-31 08:15:27.243860;2020-01-01 08:15:27.243860;2020-07-31 08:15:27.243860;export_etablissement_backup_2019_31_12_11.sql.gz;True;False \ No newline at end of file diff --git a/labonneboite/tests/importer/data/perf_prediction_and_effective_h.csv b/labonneboite/tests/importer/data/perf_prediction_and_effective_h.csv deleted file mode 100644 index 0420ab189..000000000 --- a/labonneboite/tests/importer/data/perf_prediction_and_effective_h.csv +++ /dev/null @@ -1,17 +0,0 @@ -id;ici_id;siret;naf;city_code;zip_code;departement;company_name;office_name;lbb_nb_predicted_hirings_score;lba_nb_predicted_hirings_score;lbb_nb_predicted_hirings;lba_nb_predicted_hirings;lbb_nb_effective_hirings;lba_nb_effective_hirings;is_a_bonne_boite;is_a_bonne_alternance -1;2;36252187900034;8130Z;44109;44000;44;N ;N Nantes;10;20;1;2;3;4;True;False -2;2;36252187900034;8130Z;44109;44000;44;N ;N Nantes;10;20;1;2;3;4;True;False -3;3;36252187901233;1089Z;44109;44000;44;N ;N Nantes;10;20;1;2;3;4;True;False -4;1;36252187901234;1091Z;44109;44100;44;N ;N;10;20;0;0;300;300;True;False -5;2;36252187901235;4920Z;75114;75014;75; ;P;10;20;300;300;300;300;True;False -6;3;36252187901236;8130Z;2B122;20218;49;HC;HCC;10;20;23;45;67;90;True;False -7;2;36252187901237;1089Z;97102;97121;13;GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG;GGA;10;20;300;0;300;0;True;True -8;2;36252187901238;1091Z;97102;97121;13;G ;GUA;0;10;20;300;30;300;False;True -9;2;36252187901239;4920Z;44109;44000;44; ; ;10;20;23;45;67;90;True;True -10;2;36252187901240;8130Z;44109;44100;44;LA;LA Nantes;10;20;23;45;67;90;False;True -11;2;36252187901241;1089Z;75114;75014;75;PA;0°++§§§;10;20;0;300;300;300;True;True -12;3;36252187901242;1091Z;44109;44000;44;N ;N Nantes;10;20;0;0;0;0;True;True -13;1;36252187901243;4920Z;44109;44100;44;N ;N Nantes;10;20;300;300;300;300;True;True -14;2;36252187901244;8130Z;44109;44100;44;12222;N Nantes;10;20;67;90;123;145;True;True -15;3;36252187901245;4920Z;44109;44000;44;N ;N Nantes;10;20;23;45;67;90;True;True -17;2;36252187900034;8130Z;44109;44000;44;N ;N Nantes;10;20;1;2;3;4;True;True \ No newline at end of file diff --git a/labonneboite/tests/importer/test_base.py b/labonneboite/tests/importer/test_base.py deleted file mode 100644 index e97224778..000000000 --- a/labonneboite/tests/importer/test_base.py +++ /dev/null @@ -1,33 +0,0 @@ -import os -import unittest - -from labonneboite.common.database import db_session, init_db, delete_db, engine - -from labonneboite.importer.jobs.common import logger - - -class DatabaseTest(unittest.TestCase): - """ - User and db need to be created before using this class. - User DB_USER with password DB_PASSWORD need to have all privileges on DB_NAME. - """ - - def setUp(self): - - # pylint:disable=unused-variable - # Imports are used by SQLAlchemy to know what tables to create. - from labonneboite.importer.models.computing import Hiring, DpaeStatistics, ImportTask - # pylint:enable=unused-variable - - db_session.remove() - engine.dispose() - delete_db() - init_db() - - # Mute jobs logger - logger.setLevel('CRITICAL') - - return super(DatabaseTest, self).setUp() - - def get_data_file_path(self, file_name): - return os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "data", file_name)) diff --git a/labonneboite/tests/importer/test_compute_score.py b/labonneboite/tests/importer/test_compute_score.py deleted file mode 100644 index 416909923..000000000 --- a/labonneboite/tests/importer/test_compute_score.py +++ /dev/null @@ -1,54 +0,0 @@ -from datetime import timedelta, datetime -import random - -from labonneboite.importer import compute_score -from labonneboite.importer import settings as importer_settings -from labonneboite.importer.models.computing import Hiring, RawOffice, DpaeStatistics -from .test_base import DatabaseTest - -TOTAL_OFFICES = 20 -OFFICES_HAVING_HIRINGS = 10 -YEARS_OF_HIRINGS = 5 -AVERAGE_HIRINGS_PER_MONTH_PER_OFFICE = 2 -TOTAL_HIRINGS = AVERAGE_HIRINGS_PER_MONTH_PER_OFFICE * OFFICES_HAVING_HIRINGS * 12 * YEARS_OF_HIRINGS - -def get_dpae_last_historical_data_date(): - return DpaeStatistics.get_last_historical_data_date(DpaeStatistics.DPAE) - - -def get_prediction_beginning_date(): - # add 35 days to the first of the month to be sure to be in next month - prediction_beginning_date = get_dpae_last_historical_data_date().replace(day=1) + timedelta(days=35) - # get first of the next month - return prediction_beginning_date.replace(day=1) - - -def make_offices(): - for i in range(0, TOTAL_OFFICES): - office = RawOffice(departement="57", siret=str(i), headcount="03", company_name="SNCF", - naf="2363Z", city_code="57463", zipcode="57000") - office.save() - - -def make_hirings(): - random.seed(99) # use a seed to get deterministic random numbers - for _ in range(0, TOTAL_HIRINGS): - hiring_date = get_dpae_last_historical_data_date() - timedelta(days=random.randint(1, 365*YEARS_OF_HIRINGS)) - hiring = Hiring( - siret=str(random.randint(1, OFFICES_HAVING_HIRINGS)), - departement="57", - contract_type=random.choice(Hiring.CONTRACT_TYPES_ALL), - hiring_date=hiring_date, - ) - hiring.save() - - -class TestComputeScore(DatabaseTest): - - def test_unhappy_path(self): - make_offices() - make_hirings() - departement = "58" - prediction_beginning_date = get_prediction_beginning_date() - result = compute_score.run(departement, prediction_beginning_date) - self.assertEqual(result, False) # failed computation (no data for this departement) diff --git a/labonneboite/tests/importer/test_dpae.py b/labonneboite/tests/importer/test_dpae.py deleted file mode 100644 index 9c8f0064f..000000000 --- a/labonneboite/tests/importer/test_dpae.py +++ /dev/null @@ -1,65 +0,0 @@ -from labonneboite.importer.jobs import check_dpae -from labonneboite.importer.jobs import extract_dpae -from labonneboite.importer.models.computing import Hiring, ImportTask, DpaeStatistics -from labonneboite.importer.util import get_departement_from_zipcode -from labonneboite.importer.models.errors import DoublonException -from .test_base import DatabaseTest - -FIRST_DPAE_FILE_NAME = "lbb_xdpdpae_delta_201611102200.csv" -SECOND_DPAE_FILE_NAME = "lbb_xdpdpae_delta_201612102200.csv" - -class TestDpae(DatabaseTest): - - def test_check_dpae(self): - filename = self.get_data_file_path(FIRST_DPAE_FILE_NAME) - check_dpae.check_file(filename) - self.assertEqual(Hiring.query.count(), 0) - - def test_extract_dpae(self): - self.assertEqual(Hiring.query.count(), 0) - filename = self.get_data_file_path(FIRST_DPAE_FILE_NAME) - task = extract_dpae.DpaeExtractJob(filename) - task.run() - self.assertEqual(Hiring.query.count(), 6) - # check if date_insertion is filled - self.assertEqual(Hiring.query.filter(Hiring.date_insertion == None).count(), 0) - - def test_extract_dpae_two_files_diff(self): - # Second file contains one record from the future - filename_first_month = self.get_data_file_path(FIRST_DPAE_FILE_NAME) - filename_second_month = self.get_data_file_path(SECOND_DPAE_FILE_NAME) - task = extract_dpae.DpaeExtractJob(filename_first_month) - task.run() - self.assertEqual(Hiring.query.count(), 6) - task = extract_dpae.DpaeExtractJob(filename_second_month) - task.run() - # change 6+5 to 6+2, only 2 dpae is between 10/11/2016 and 10/12/2016 in SECOND_DPAE_FILE_NAME - self.assertEqual(Hiring.query.count(), 6+2) - - def test_verify_right_number_dpae(self): - self.assertEqual(Hiring.query.count(), 0) - filename = self.get_data_file_path(FIRST_DPAE_FILE_NAME) - task = extract_dpae.DpaeExtractJob(filename) - task.run() - self.assertEqual(Hiring.query.count(), 6) - # delete the file in the registry to simulate if importer job crash before regitring the file - ImportTask.query.filter(ImportTask.filename == FIRST_DPAE_FILE_NAME).delete() - DpaeStatistics.query.order_by(DpaeStatistics.most_recent_data_date.desc()).first().delete() - task = extract_dpae.DpaeExtractJob(filename) - self.assertEqual(Hiring.query.count(), 6) - - def test_extract_departement(self): - departement = get_departement_from_zipcode("6600") - self.assertEqual(departement, "06") - - def test_extract_gz_format(self): - filename = self.get_data_file_path(FIRST_DPAE_FILE_NAME + ".gz") - task = extract_dpae.DpaeExtractJob(filename) - task.run() - self.assertEqual(Hiring.query.count(), 6) - - def test_extract_bz2_format(self): - filename = self.get_data_file_path(FIRST_DPAE_FILE_NAME + ".bz2") - task = extract_dpae.DpaeExtractJob(filename) - task.run() - self.assertEqual(Hiring.query.count(), 6) diff --git a/labonneboite/tests/importer/test_etablissements.py b/labonneboite/tests/importer/test_etablissements.py deleted file mode 100644 index 254dd1acc..000000000 --- a/labonneboite/tests/importer/test_etablissements.py +++ /dev/null @@ -1,114 +0,0 @@ -from labonneboite.importer.models.computing import RawOffice -from labonneboite.importer.jobs.extract_etablissements import EtablissementExtractJob, normalize_website_url -from .test_base import DatabaseTest - - -def make_raw_office(): - office = RawOffice( - siret="12345678901234", - company_name="SNCF", - street_number="30", - street_name="rue Edouard Poisson", - zipcode="93300", - city_code="93001", - departement="57", - headcount="11", - naf="2363Z", - ) - office.save() - -ETABLISSEMENT_FILE = "lbb_etablissement_full_201612192300.csv" - -class TestEtablissements(DatabaseTest): - - def test_get_sirets_from_database(self): - filename = self.get_data_file_path(ETABLISSEMENT_FILE) - task = EtablissementExtractJob(filename) - etabs = task.get_sirets_from_database() - self.assertEqual(len(etabs), 0) - make_raw_office() - etabs = task.get_sirets_from_database() - self.assertEqual(len(etabs), 1) - self.assertEqual(etabs[0], "12345678901234") - - def test_get_offices_from_file(self): - filename = self.get_data_file_path(ETABLISSEMENT_FILE) - task = EtablissementExtractJob(filename) - etabs = {} - for off in task.get_offices_from_file(): - etabs.update(off) - task.csv_offices = etabs - self.assertEqual(len(list(etabs.keys())), 26) - _, raisonsociale, _, _, _, _, \ - _, _, email, _, _, _, \ - _, _, _ = etabs.get('26560004900167').get('create_fields') - self.assertEqual(raisonsociale, 'CTRE HOSPITALIER JOSSELIN') - self.assertEqual(email, '') - _, raisonsociale, _, _, _, _, \ - _, _, email, _, _, _, \ - _, _, _ = etabs.get('26560004900267').get('create_fields') - self.assertEqual(raisonsociale, 'POLE EMPLOI') - self.assertEqual(email, 'origin_email@pole-emploi.fr') - - def test_create_new_offices(self): - filename = self.get_data_file_path(ETABLISSEMENT_FILE) - task = EtablissementExtractJob(filename) - - csv_offices = {} - for off in task.get_offices_from_file(): - csv_offices.update(off) - task.csv_offices = csv_offices - task.creatable_sirets = [ - "00565014800033", "00685016800011" - ] - task.create_update_offices() - self.assertEqual(len(RawOffice.query.all()), 2) - - def test_delete_offices(self): - filename = self.get_data_file_path(ETABLISSEMENT_FILE) - task = EtablissementExtractJob(filename) - csv_offices = {} - for off in task.get_offices_from_file(): - csv_offices.update(off) - task.csv_offices = csv_offices - task.creatable_sirets = [ - "00565014800033", "00685016800011" - ] - task.create_update_offices() - task.deletable_sirets = set(["00565014800033"]) - task.delete_deletable_offices() - self.assertEqual(len(RawOffice.query.all()), 1) - self.assertEqual(RawOffice.query.first().siret, "00685016800011") - - def test_normalize_url(self): - self.assertEqual(normalize_website_url(None), None) - self.assertEqual(normalize_website_url(''), None) - self.assertEqual(normalize_website_url('abc'), None) - self.assertEqual(normalize_website_url('abc.com'), 'http://abc.com') - self.assertEqual(normalize_website_url('abc.fr'), 'http://abc.fr') - self.assertEqual(normalize_website_url('http://abc.fr'), 'http://abc.fr') - self.assertEqual(normalize_website_url('https://abc.fr'), 'https://abc.fr') - self.assertEqual(normalize_website_url('abc@def.fr'), None) - - def test_emails_rgpd(self): - filename = self.get_data_file_path(ETABLISSEMENT_FILE) - task = EtablissementExtractJob(filename) - etabs = {} - for off in task.get_offices_from_file(): - etabs.update(off) - task.csv_offices = etabs - - _, _, _, _, _, _, \ - _, _, email, _, _, _, \ - _, _, _ = etabs.get('00565014800033').get('create_fields') - self.assertEqual(email, 'laf@example.eu') #The office has 2 emails, and the rgpd email is the main - - _, _, _, _, _, _, \ - _, _, email, _, _, _, \ - _, _, _ = etabs.get('48874364200024').get('create_fields') - self.assertEqual(email, 'f.renier@abcdef.tm.fr') #The office has only one email, no rgpd - - _, _, _, _, _, _, \ - _, _, email, _, _, _, \ - _, _, _ = etabs.get('42086672500021').get('create_fields') - self.assertEqual(email, 'frederic.fleury@abcdef.fr') #The office has only one email, rgpd diff --git a/labonneboite/tests/importer/test_geocode.py b/labonneboite/tests/importer/test_geocode.py deleted file mode 100644 index 46e5cc715..000000000 --- a/labonneboite/tests/importer/test_geocode.py +++ /dev/null @@ -1,73 +0,0 @@ -from labonneboite.importer.models.computing import ExportableOffice -from labonneboite.importer.jobs.geocode import GeocodeJob -from .test_base import DatabaseTest - - -def make_geocoded_office(): - office = ExportableOffice( - siret=1234, - company_name="SNCF", - street_number="30", - street_name="rue Edouard Poisson", - zipcode="93300", - city_code="93001", - departement="57", - headcount="11", - naf="2363Z", - x=1.1, - y=1.1, - ) - office.save() - - -class TestGeocode(DatabaseTest): - - def test_run_geocoding_job(self): - task = GeocodeJob() - initial_coordinates = [0, 0] - jobs = [[1234, "1 rue Marca 64000 Pau", initial_coordinates, '64445']] - task.run_geocoding_jobs(jobs, disable_multithreading=True) - updates = task.run_missing_geocoding_jobs(disable_multithreading=True) - self.assertTrue(len(updates), 1) - coordinates = updates[0][1] - self.assertEqual(int(coordinates[0]), 0) - self.assertEqual(int(coordinates[1]), 43) - - def test_run_geocoding_jobs(self): - task = GeocodeJob() - initial_coordinates = [0, 0] - jobs = [['001234', "1 rue Marca 64000 Pau", initial_coordinates, '64445'], - ['005678', "13 rue de l'hotel de ville 44000 Nantes", initial_coordinates, '44109']] - task.run_geocoding_jobs(jobs, disable_multithreading=True) - updates = task.run_missing_geocoding_jobs( - csv_max_rows=1, disable_multithreading=True) - self.assertTrue(len(updates), 2) - coordinates_1 = updates[0] - coordinates_2 = updates[1] - # We want to test this because we had an issue, where the pandas dataframes changes types of siret to int, and the '00' at the start of siret was removed - self.assertTrue(len(coordinates_1[0]), 6) - - self.assertEqual(int(coordinates_1[1][0]), 0) - self.assertEqual(int(coordinates_1[1][1]), 43) - self.assertEqual(int(coordinates_2[1][0]), -1) - self.assertEqual(int(coordinates_2[1][1]), 47) - - def test_create_geocoding_jobs(self): - task = GeocodeJob() - jobs = task.create_geocoding_jobs() - self.assertEqual(len(jobs), 0) - make_geocoded_office() - jobs = task.create_geocoding_jobs() - self.assertEqual(len(jobs), 1) - self.assertEqual(jobs[0][0], "1234") - self.assertEqual( - jobs[0][1], "30 rue Edouard Poisson 93300 AUBERVILLIERS") - - def test_update_coordinates(self): - make_geocoded_office() - task = GeocodeJob() - updates = [["1234", [0, 43]]] - task.update_coordinates(updates) - office = ExportableOffice.query.first() - self.assertEqual(int(office.x), 0) - self.assertEqual(int(office.y), 43) diff --git a/labonneboite/tests/importer/test_perf_compute_data.py b/labonneboite/tests/importer/test_perf_compute_data.py deleted file mode 100644 index d402cc391..000000000 --- a/labonneboite/tests/importer/test_perf_compute_data.py +++ /dev/null @@ -1,134 +0,0 @@ -from labonneboite.importer.jobs.performance_compute_data import (lancement_requete, - PayloadDataframe,prepare_google_sheet_data) -from labonneboite.importer.models.computing import PerfImporterCycleInfos, PerfDivisionPerRome, PerfPredictionAndEffectiveHirings -from .test_base import DatabaseTest -from labonneboite.common import load_data -from datetime import datetime - -result_df_global = {'cycle': [1, 2, 3], - 'nbTotalHirings': [600.0, 1196.0, 137.0], - 'nbTotal': [2, 10, 4], - 'sum10': [0.0, 0.2508361204013378, 0.0], - 'sum20': [0.0, 0.5016722408026756, 0.0], - 'sum30': [0.0, 0.6045150501672241, 0.48905109489051096], - 'sum40': [0.0, 0.6605351170568562, 0.48905109489051096], - 'sum50': [0.5, 0.7165551839464883, 0.9781021897810219], - 'sum60': [0.5, 0.7416387959866221, 0.9781021897810219], - 'sum70': [0.5, 0.7441471571906354, 0.9781021897810219], - 'sum80': [0.5, 0.7466555183946488, 1.0], - 'sum90': [0.5, 0.7491638795986622, 1.0], - 'RMSE': [212.13203435596427, 98.54947995803936, 31.12876483254676], - 'nbTotalLBXHirings': [600.0, 1099.0, 137.0], - 'nbTotalLBX': [2, 8, 4], - 'propRecrutNonLBX': [0.0, 0.08110367892976589, 0.0]} - - -result_df_naf = {'cycle': [1, 1, 2, 2, 2, 3, 3, 3, 3], - 'naf': ['1091Z', '4920Z', '1089Z', '4920Z', '8130Z', '1089Z', '1091Z', '4920Z', '8130Z'], - 'nbTotalHirings': [300.0, 300.0, 600.0, 367.0, 199.0, 3.0, 0.0, 67.0, 67.0], - 'nbTotal': [1, 1, 2, 2, 5, 1, 1, 1, 1], - 'sum10': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - 'sum20': [0.0, 0.0, 0.0, 0.0, 0.6180904522613065, 0.0, 0.0, 0.0, 0.0], - 'sum30': [0.0, 0.0, 0.0, 0.0, 0.6180904522613065, 0.0, 0.0, 0.0, 0.0], - 'sum40': [0.0, 0.0, 0.0, 0.0, 0.9547738693467337, 0.0, 0.0, 0.0, 0.0], - 'sum50': [0.0, 0.0, 0.5, 0.8174386920980926, 0.9547738693467337, 0.0, 0.0, 0.0, 0.0], - 'sum60': [0.0, 0.0, 0.5, 0.8174386920980926, 0.9698492462311558, 0.0, 0.0, 0.0, 0.0], - 'sum70': [0.0, 0.0, 0.5, 0.8174386920980926, 0.9698492462311558, 0.0, 0.0, 0.0, 0.0], - 'sum80': [0.0, 0.0, 0.5, 0.8174386920980926, 0.9849246231155779, 0.0, 0.0, 0.0, 0.0], - 'sum90': [0.0, 0.0, 0.5, 0.8174386920980926, 0.9849246231155779, 0.0, 0.0, 0.0, 0.0], - 'RMSE': [300.0, 0.0, 212.13203435596427, 31.11269837220809, 31.887301547794852, 2.0, 0.0, 44.0, 44.0], - 'nbTotalLBXHirings': [300.0, 300.0, 600.0, 367.0, 132.0, 3.0, 0.0, 67.0, 67.0], - 'nbTotalLBX': [1, 1, 2, 2, 4, 1, 1, 1, 1], - 'propRecrutNonLBX': [0.0, 0.0, 0.0, 0.0, 0.33668341708542715, 0.0, 1, 0.0, 0.0]} - -result_df_dep = {'cycle': [1, 2, 2, 2, 3, 3], - 'dep': ['44', '13', '44', '75', '44', '49'], - 'nbTotalHirings': [600.0, 330.0, 266.0, 600.0, 70.0, 67.0], - 'nbTotal': [2, 2, 6, 2, 3, 1], - 'sum10': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - 'sum20': [0.0, 0.0, 0.462406015037594, 0.0, 0.0, 0.0], - 'sum30': [0.0, 0.0, 0.462406015037594, 0.0, 0.0, 0.0], - 'sum40': [0.0, 0.0, 0.7142857142857143, 0.0, 0.9571428571428572, 0.0], - 'sum50': [0.5, 0.9090909090909091, 0.9661654135338346, 0.5, 0.9571428571428572, 0.0], - 'sum60': [0.5, 0.9090909090909091, 0.9661654135338346, 0.5, 0.9571428571428572, 0.0], - 'sum70': [0.5, 0.9090909090909091, 0.9774436090225563, 0.5, 1.0, 0.0], - 'sum80': [0.5, 0.9090909090909091, 0.9774436090225563, 0.5, 1.0, 0.0], - 'sum90': [0.5, 0.9090909090909091, 0.9887218045112782, 0.5, 1.0, 0.0], - 'RMSE': [212.13203435596427, 7.0710678118654755, 34.20526275297414, 212.13203435596427, 25.4296414970142, 44.0], - 'nbTotalLBXHirings': [600.0, 300.0, 199.0, 600.0, 70.0, 67.0], - 'nbTotalLBX': [2, 1, 5, 2, 3, 1], - 'propRecrutNonLBX': [0.0, 0.09090909090909091, 0.2518796992481203, 0.0, 0.0, 0.0] - } - - -def load_csv_perf_division_per_rome(filename, delimiter=';'): - - for row in load_data.load_csv_file(filename, delimiter): - perf_div_per_rome = PerfDivisionPerRome( - _id=row[0], - importer_cycle_infos_id=row[1], - naf=row[3], - rome=row[2], - threshold_lbb=row[4], - nb_bonne_boites_lbb=row[5], - threshold_lba=row[6], - nb_bonne_boites_lba=row[7] - ) - perf_div_per_rome.save() - - -def load_csv_perf_importer_cycle_infos(filename, delimiter=';'): - for row in load_data.load_csv_file(filename, delimiter): - perf_importer_cycle_info = PerfImporterCycleInfos( - _id=row[0], - execution_date=datetime.strptime(row[1], '%Y-%m-%d %H:%M:%S.%f'), - prediction_start_date=datetime.strptime(row[2], '%Y-%m-%d %H:%M:%S.%f'), - prediction_end_date=datetime.strptime(row[3], '%Y-%m-%d %H:%M:%S.%f'), - file_name=row[4], - computed=(row[5] == 'True'), - on_google_sheets=(row[6] == 'True') - ) - perf_importer_cycle_info.save() - - -def load_csv_perf_prediction_and_effective_h(filename, delimiter=';'): - for row in load_data.load_csv_file(filename, delimiter): - perf_importer_cycle_info = PerfPredictionAndEffectiveHirings( - _id=row[0], - importer_cycle_infos_id=row[1], - siret=row[2], - naf=row[3], - city_code=row[4], - zipcode=row[5], - departement=row[6], - company_name=row[7], - office_name=row[8], - lbb_nb_predicted_hirings_score=row[9], - lba_nb_predicted_hirings_score=row[10], - lbb_nb_predicted_hirings=row[11], - lba_nb_predicted_hirings=row[12], - lbb_nb_effective_hirings=row[13], - lba_nb_effective_hirings=row[14], - is_a_bonne_boite=(row[15] == "True"), - is_a_bonne_alternance=(row[16] == "True") - ) - perf_importer_cycle_info.save() - - -def load_data_set_up(): - load_csv_perf_importer_cycle_infos("../../tests/importer/data/perf_importer_cycle_infos.csv") - load_csv_perf_division_per_rome("../../tests/importer/data/perf_division_per_rome.csv") - load_csv_perf_prediction_and_effective_h("../../tests/importer/data/perf_prediction_and_effective_h.csv") - - -class TestPerfComputeData(DatabaseTest): - - def test_lancement_requete(self): - load_data_set_up() - pdf = PayloadDataframe() - df_global = lancement_requete(pdf, "global", is_lbb=True) - df_naf = lancement_requete(pdf, "codenaf", "naf", is_lbb=True) - df_dep = lancement_requete(pdf, "departement", "dep", is_lbb=True) - for df, results in [(df_naf,result_df_naf), (df_dep, result_df_dep), (df_global, result_df_global)]: - for column in results.keys(): - self.assertTrue(results[column] == df[column].tolist()) diff --git a/labonneboite/tests/importer/test_perf_insert_data.py b/labonneboite/tests/importer/test_perf_insert_data.py deleted file mode 100644 index 0efb65148..000000000 --- a/labonneboite/tests/importer/test_perf_insert_data.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -import datetime - -from .test_base import DatabaseTest -from labonneboite.importer.jobs.performance_insert_data import (get_available_files_list, - insert_into_sql_table_old_prediction_file, - insert_data, - compute_effective_and_predicted_hirings) -from labonneboite.importer.models.computing import PerfImporterCycleInfos, PerfPredictionAndEffectiveHirings -from labonneboite.importer import util as import_util - - -def make_fake_perf_importer_cycles_infos_id(): - cycle_infos = PerfImporterCycleInfos( - execution_date=datetime.datetime.now(), - prediction_start_date=datetime.datetime.now(), - prediction_end_date=datetime.datetime.now(), - file_name="dummy_export_etablissement_backup_2020_11_12_1746.sql.gz", - computed=False, - on_google_sheets=False - ) - cycle_infos.save() - - -class TestPerfInsertData(DatabaseTest): - - def test_get_available_files_list(self): - make_fake_perf_importer_cycles_infos_id() - files_list = get_available_files_list(path_folder=os.path.join(os.path.dirname(__file__), - "data")) - self.assertTrue(len(files_list) == 1) - self.assertTrue("export_etablissement_backup_2019_11_10_1716.sql.gz" in files_list[0]) - - def test_compute_data(self): - compute_effective_and_predicted_hirings() - fields_not_null = ["lbb_nb_predicted_hirings", - "lba_nb_predicted_hirings", - "lbb_nb_effective_hirings", - "lba_nb_effective_hirings", - "is_a_bonne_boite", - "is_a_bonne_alternance"] - for ppaeh in PerfPredictionAndEffectiveHirings.query.all(): - for field in fields_not_null: - self.assertTrue(getattr(ppaeh, field) is not None) diff --git a/labonneboite/tests/importer/test_scoring.py b/labonneboite/tests/importer/test_scoring.py deleted file mode 100644 index 1e9d541c6..000000000 --- a/labonneboite/tests/importer/test_scoring.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -To be executed after a scoring (update_lbb_data) to make sure the well scored companies have no DPAE. -""" -from datetime import datetime, timedelta - -from sqlalchemy import and_ - -from labonneboite.common import scoring as scoring_util -from labonneboite.common.models import Office -from labonneboite.conf import settings -from labonneboite.importer.models.computing import Hiring -from .test_base import DatabaseTest - - -FIFTEEN_MONTHS = 15 * 30 - - -class ScoringTest(DatabaseTest): - - # ############### WARNING about matching scores vs hirings ################ - # Methods scoring_util.get_hirings_from_score - # and scoring_util.get_score_from_hirings - # rely on special coefficients SCORE_50_HIRINGS, SCORE_60_HIRINGS etc.. - # which values in github repository are *fake* and used for dev and test only. - # - # The real values are confidential, stored outside of github repo, - # and only used in staging and production. - # - # This is designed so that you *CANNOT* guess the hirings based - # on the score you see in production. - # ######################################################################### - - def test_key_values_of_conversion_between_score_and_hirings(self): - self.assertEqual(0, scoring_util.get_score_from_hirings(0)) - self.assertEqual(50, scoring_util.get_score_from_hirings(settings.SCORE_50_HIRINGS)) - self.assertEqual(60, scoring_util.get_score_from_hirings(settings.SCORE_60_HIRINGS)) - self.assertEqual(80, scoring_util.get_score_from_hirings(settings.SCORE_80_HIRINGS)) - self.assertEqual(100, scoring_util.get_score_from_hirings(settings.SCORE_100_HIRINGS)) diff --git a/labonneboite/tests/scripts/test_create_index.py b/labonneboite/tests/scripts/test_create_index.py index 3ac378996..836e4db8e 100644 --- a/labonneboite/tests/scripts/test_create_index.py +++ b/labonneboite/tests/scripts/test_create_index.py @@ -145,16 +145,17 @@ def test_office_admin_add(self): # Login as user admin self.user = db_session.query(User).filter_by(id=self.user.id).first() self.assertEqual(db_session.query(User).count(), 1) - self.login(self.user) - # Create OfficeAdminRemove - self.assertEqual(0, OfficeAdminAdd.query.filter_by(id=1).count()) - self.app.post(url_for('officeadminadd.create_view'), data=form) - self.assertEqual(1, OfficeAdminAdd.query.filter_by(id=1).count()) + with self.login_client.test_client(user=self.user) as client: - # Delete OfficeAdminAdd - self.app.post(url_for('officeadminadd.delete_view'), data={'id': 1}) - self.assertEqual(0, OfficeAdminRemove.query.filter_by(id=1).count()) + # Create OfficeAdminRemove + self.assertEqual(0, OfficeAdminAdd.query.filter_by(id=1).count()) + client.post(url_for('officeadminadd.create_view'), data=form) + self.assertEqual(1, OfficeAdminAdd.query.filter_by(id=1).count()) + + # Delete OfficeAdminAdd + client.post(url_for('officeadminadd.delete_view'), data={'id': 1}) + self.assertEqual(0, OfficeAdminRemove.query.filter_by(id=1).count()) def test_office_admin_remove(self): # Create officeAdminRemove @@ -187,16 +188,17 @@ def test_office_admin_remove(self): # Login as user admin self.user = db_session.query(User).filter_by(id=self.user.id).first() self.assertEqual(db_session.query(User).count(), 1) - self.login(self.user) - # Create OfficeAdminRemove - self.assertEqual(0, OfficeAdminRemove.query.filter_by(siret='01234567891234').count()) - self.app.post(url_for('officeadminremove.create_view'), data=form) - self.assertEqual(1, OfficeAdminRemove.query.filter_by(siret='01234567891234').count()) + with self.login_client.test_client(user=self.user) as client: + + # Create OfficeAdminRemove + self.assertEqual(0, OfficeAdminRemove.query.filter_by(siret='01234567891234').count()) + client.post(url_for('officeadminremove.create_view'), data=form) + self.assertEqual(1, OfficeAdminRemove.query.filter_by(siret='01234567891234').count()) - # Delete OfficeAdminRemove - self.app.post(url_for('officeadminremove.delete_view'), data={'id': 1}) - self.assertEqual(0, OfficeAdminRemove.query.filter_by(id=1).count()) + # Delete OfficeAdminRemove + client.post(url_for('officeadminremove.delete_view'), data={'id': 1}) + self.assertEqual(0, OfficeAdminRemove.query.filter_by(id=1).count()) class VariousModesTest(CreateIndexBaseTest): diff --git a/labonneboite/tests/selenium/base.py b/labonneboite/tests/selenium/base.py index b8b502d8f..716465e45 100644 --- a/labonneboite/tests/selenium/base.py +++ b/labonneboite/tests/selenium/base.py @@ -34,18 +34,17 @@ def create_app(self): settings.API_DEPARTMENTS_URL = 'https://geo.api.gouv.fr/departements' # Random port generation - app.config['LIVESERVER_PORT'] = 0 + app.config['LIVESERVER_PORT'] = 8943 app.config['SERVER_NAME'] = None + app.config['TESTING'] = True # Disable logging app.logger.setLevel(logging.CRITICAL) logging.getLogger('werkzeug').setLevel(logging.CRITICAL) logging.getLogger('easyprocess').setLevel(logging.CRITICAL) - return app def setUp(self): super(LbbSeleniumTestCase, self).setUp() - self.display = None try: from pyvirtualdisplay import Display diff --git a/labonneboite/tests/selenium/test_make_a_new_search_on_search_page.py b/labonneboite/tests/selenium/test_make_a_new_search_on_search_page.py index 8d47c3f27..e3b630c79 100644 --- a/labonneboite/tests/selenium/test_make_a_new_search_on_search_page.py +++ b/labonneboite/tests/selenium/test_make_a_new_search_on_search_page.py @@ -1,5 +1,6 @@ import time import re +import pytest from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait @@ -13,6 +14,7 @@ class TestMakeANewSearchOnSearchPage(LbbSeleniumTestCase): def setUp(self): + super().setUp() url = self.url_for( 'search.entreprises', @@ -32,10 +34,11 @@ def fail_if_no_results(self): Fail if there is no result to the search This is a check to make sure that the problem lies in the data or in the code """ - results_sentence = self.driver.find_element_by_css_selector('body').text + results_sentence = self.driver.find_element(By.CSS_SELECTOR, 'body').text self.assertNotIn("Nous n'avons pas de résultat", results_sentence, 'There is no result for the current search (' + self.driver.current_url + ')') + @pytest.mark.skip def test_make_a_new_search_changing_location(self): """ Test that a user can change location directly @@ -54,16 +57,16 @@ def test_make_a_new_search_changing_location(self): results_sentence = self.driver.find_element(*title_selector).text primitive_results = re.match(r'(\d+)', results_sentence).group() - shown_search_form = self.driver.find_element_by_css_selector('#shown-search-form') + shown_search_form = self.driver.find_element(By.CSS_SELECTOR, '#shown-search-form') - location_field = shown_search_form.find_element_by_css_selector('#l') + location_field = shown_search_form.find_element(By.CSS_SELECTOR, '#l') location_field.clear() location_field.send_keys(city) time.sleep(2) location_field.send_keys(Keys.DOWN) location_field.send_keys(Keys.RETURN) - shown_search_form.find_element_by_css_selector('button').click() + shown_search_form.find_element(By.CSS_SELECTOR, 'button').click() try: wait.until(EC.url_changes(current_url)) @@ -94,26 +97,26 @@ def test_make_a_new_search_changing_occupation(self): in a search results page using a form. """ occupation = 'Boucher' - results_sentence = self.driver.find_element_by_css_selector('h1.lbb-result-info').text + results_sentence = self.driver.find_element(By.CSS_SELECTOR, 'h1.lbb-result-info').text primitive_results = re.match(r'(\d+)', results_sentence).group() - shown_search_form = self.driver.find_element_by_css_selector('#shown-search-form') + shown_search_form = self.driver.find_element(By.CSS_SELECTOR, '#shown-search-form') - occupation_field = shown_search_form.find_element_by_css_selector('#j') + occupation_field = shown_search_form.find_element(By.CSS_SELECTOR, '#j') occupation_field.clear() occupation_field.send_keys(occupation) time.sleep(2) occupation_field.send_keys(Keys.DOWN) occupation_field.send_keys(Keys.RETURN) - shown_search_form.find_element_by_css_selector('button').click() + shown_search_form.find_element(By.CSS_SELECTOR, 'button').click() WebDriverWait(self.driver, 60)\ .until( EC.visibility_of_element_located((By.CSS_SELECTOR, "h1.lbb-result-info")) ) - results_sentence = self.driver.find_element_by_css_selector('h1.lbb-result-info').text + results_sentence = self.driver.find_element(By.CSS_SELECTOR, 'h1.lbb-result-info').text last_results = re.match(r'(\d+)', results_sentence).group() self.assertIn(occupation, results_sentence) diff --git a/labonneboite/tests/selenium/test_reset_naf.py b/labonneboite/tests/selenium/test_reset_naf.py index c06cfe6cf..39c28250c 100644 --- a/labonneboite/tests/selenium/test_reset_naf.py +++ b/labonneboite/tests/selenium/test_reset_naf.py @@ -1,6 +1,7 @@ import time import urllib.parse +from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.support.ui import Select from selenium.webdriver.support.ui import WebDriverWait @@ -33,7 +34,7 @@ def test_reset_naf(self): self.assertNotIn('naf', parameters) # Filter by NAF `Activités des agents et courtiers d'assurances` (`6622Z`). - select = Select(self.driver.find_element_by_id('naf')) + select = Select(self.driver.find_element(By.ID, 'naf')) select.select_by_value('6622Z') WebDriverWait(self.driver, 60).until(url_has_changed(current_url)) @@ -47,7 +48,7 @@ def test_reset_naf(self): self.assertEqual('6622Z', parameters['naf']) # Perform another search on `boucher`. - job_input = self.driver.find_element_by_name('j') + job_input = self.driver.find_element(By.NAME, 'j') job_input.clear() # Reset the previous `comptabilite` search term. job_input.send_keys('boucher') time.sleep(3) @@ -59,7 +60,7 @@ def test_reset_naf(self): # self.driver.find_element_by_id('flHideToolBarButton').click() # Submit the search form. - self.driver.find_element_by_css_selector('#shown-search-form button').click() + self.driver.find_element(By.CSS_SELECTOR, '#shown-search-form button').click() # The NAF filter should be reset. WebDriverWait(self.driver, 60).until(url_has_changed(current_url)) diff --git a/labonneboite/tests/selenium/test_results.py b/labonneboite/tests/selenium/test_results.py index be2d656ce..759ba951d 100644 --- a/labonneboite/tests/selenium/test_results.py +++ b/labonneboite/tests/selenium/test_results.py @@ -1,7 +1,7 @@ import time from .base import LbbSeleniumTestCase - +from selenium.webdriver.common.by import By class TestResults(LbbSeleniumTestCase): @@ -18,27 +18,27 @@ def test_toggle_office_details(self): self.driver.get(url) # Get the HTML element that contains all company informations. - company_container = self.driver.find_elements_by_class_name('lbb-company')[0] + company_container = self.driver.find_elements(By.CLASS_NAME, 'lbb-company')[0] time.sleep(0.5) # Inspect default state. self.assertNotIn('active', company_container.get_attribute('class').split(' ')) - for element in company_container.find_elements_by_class_name('lbb-result__details'): + for element in company_container.find_elements(By.CLASS_NAME, 'lbb-result__details'): self.assertEqual(element.value_of_css_property('display'), 'none') - toggle_details = self.driver.find_elements_by_class_name('js-result-toggle-details')[0] + toggle_details = self.driver.find_elements(By.CLASS_NAME, 'js-result-toggle-details')[0] time.sleep(0.5) # Display company details. toggle_details.click() time.sleep(0.5) self.assertIn('active', company_container.get_attribute('class').split(' ')) - for element in company_container.find_elements_by_class_name('lbb-result__details'): + for element in company_container.find_elements(By.CLASS_NAME, 'lbb-result__details'): self.assertEqual(element.value_of_css_property('display'), 'block') # Hide company details. toggle_details.click() time.sleep(0.5) self.assertNotIn('active', company_container.get_attribute('class').split(' ')) - for element in company_container.find_elements_by_class_name('lbb-result__details'): + for element in company_container.find_elements(By.CLASS_NAME, 'lbb-result__details'): self.assertEqual(element.value_of_css_property('display'), 'none') diff --git a/labonneboite/tests/selenium/test_simple.py b/labonneboite/tests/selenium/test_simple.py index 647577b13..6e8a1186d 100644 --- a/labonneboite/tests/selenium/test_simple.py +++ b/labonneboite/tests/selenium/test_simple.py @@ -18,9 +18,9 @@ def test_search(self): wait = WebDriverWait(self.driver, 5) self.driver.get(self.url_for('root.home')) - job_element = self.driver.find_element_by_id('j') - location_element = self.driver.find_element_by_id('l') - submit_element = self.driver.find_element_by_css_selector('.lbb-home-form-search button') + job_element = self.driver.find_element(By.ID, 'j') + location_element = self.driver.find_element(By.ID, 'l') + submit_element = self.driver.find_element(By.CSS_SELECTOR, '.lbb-home-form-search button') self.assertTrue(submit_element.is_enabled(), 'By default the submit button should be enable') @@ -37,5 +37,5 @@ def test_search(self): self.assertElementIsFocus(submit_element, 'When selecting a location, the submit button should be focus') submit_element.click() - elements = self.driver.find_elements_by_class_name('lbb-company') + elements = self.driver.find_elements(By.CLASS_NAME, 'lbb-company') self.assertEqual(1, len(elements)) diff --git a/labonneboite/tests/test_base.py b/labonneboite/tests/test_base.py index 24f31f177..3770e88d7 100644 --- a/labonneboite/tests/test_base.py +++ b/labonneboite/tests/test_base.py @@ -2,7 +2,7 @@ import unittest from flask import url_for as flask_url_for -from flask import _request_ctx_stack +from flask_login import FlaskLoginClient from labonneboite.common.database import db_session, delete_db, engine, init_db from labonneboite.common import env @@ -30,9 +30,14 @@ class AppTest(unittest.TestCase): """ def setUp(self): + self.app = app.test_client() self.app_context = app.app_context self.test_request_context = app.test_request_context + + self.login_client = app + self.login_client.test_client_class = FlaskLoginClient + # Disable logging app.logger.setLevel(logging.CRITICAL) @@ -46,38 +51,6 @@ def url_for(self, endpoint, **kwargs): url = flask_url_for(endpoint, **kwargs) return url - def login(self, user, social_auth_backend='peam-openidconnect'): - """ - Logs a user in by simulating a third-party authentication process. - - This method should always be called within the same request context - as the test that uses it in order to use the same session object: - with self.test_request_context(): - self.login(user) - ... - """ - _request_ctx_stack.top.user = user - with self.app.session_transaction() as sess: - # Session info set by Flask-Login. - sess['user_id'] = user.id - # Session info set by Python Social Auth. - sess['social_auth_last_login_backend'] = social_auth_backend - sess['%s_state' % social_auth_backend] = 'a1z2e3r4t5y6y' - - def logout(self): - """ - Logs a user out. - - This method should always be called within the same request context - as the test that uses it in order to use the same session object: - with self.test_request_context(): - ... - self.logout() - """ - self.app.get('/authentication/logout') - del _request_ctx_stack.top.user - - class DatabaseTest(AppTest): """ Configure MySQL and Elasticsearch for unit tests. diff --git a/labonneboite/tests/web/front/test_admin.py b/labonneboite/tests/web/front/test_admin.py index a2aee9ff0..7d260629b 100644 --- a/labonneboite/tests/web/front/test_admin.py +++ b/labonneboite/tests/web/front/test_admin.py @@ -67,7 +67,10 @@ def test_admin_access(self): self.url_for('officeadminextrageolocation.index_view'), ] - with self.test_request_context(): + db_session.query(User).update({User.active: True, User.is_admin: False}) + db_session.commit() + self.user = db_session.query(User).filter_by(id=self.user.id).first() + with self.login_client.test_client(user=self.user) as client: for url in admin_urls: @@ -77,13 +80,11 @@ def test_admin_access(self): self.user = db_session.query(User).filter_by(id=self.user.id).first() self.assertTrue(self.user.active) self.assertFalse(self.user.is_admin) - rv = self.app.get(url) + rv = client.get(url) self.assertEqual(rv.status_code, 404) - self.login(self.user) - # Access should be denied when a user is logged in but is not an admin. - rv = self.app.get(url) + rv = client.get(url) self.assertEqual(rv.status_code, 404) # Access should be granted when a user is logged in and is admin. @@ -92,7 +93,8 @@ def test_admin_access(self): self.user = db_session.query(User).filter_by(id=self.user.id).first() self.assertTrue(self.user.active) self.assertTrue(self.user.is_admin) - rv = self.app.get(url) + + rv = client.get(url) self.assertEqual(rv.status_code, 200) # Access should be denied when a user is not active. @@ -101,11 +103,9 @@ def test_admin_access(self): self.user = db_session.query(User).filter_by(id=self.user.id).first() self.assertFalse(self.user.active) self.assertTrue(self.user.is_admin) - rv = self.app.get(url) + rv = client.get(url) self.assertEqual(rv.status_code, 404) - self.logout() - def test_admin_office_remove(self): """ Test `OfficeAdminRemoveModelView.after_model_change()` to delete an office diff --git a/labonneboite/tests/web/front/test_auth.py b/labonneboite/tests/web/front/test_auth.py index bace226ca..c5f861293 100644 --- a/labonneboite/tests/web/front/test_auth.py +++ b/labonneboite/tests/web/front/test_auth.py @@ -30,24 +30,30 @@ def test_logout(self): db_session.add(user_social_auth) db_session.commit() - with self.test_request_context(): + with self.login_client.test_client(user=user) as client: - with self.app.session_transaction() as sess: + with client.session_transaction() as sess: sess['this_should_not_be_deleted'] = 'foo' # This should not be deleted by a login or logout. - self.login(user) + with client.session_transaction() as sess: + social_auth_backend = 'peam-openidconnect' + # Session info set by Flask-Login. + sess['_user_id'] = user.id + # Session info set by Python Social Auth. + sess['social_auth_last_login_backend'] = social_auth_backend + sess['%s_state' % social_auth_backend] = 'a1z2e3r4t5y6y' - with self.app.session_transaction() as sess: + with client.session_transaction() as sess: self.assertIn('this_should_not_be_deleted', sess) - self.assertIn('user_id', sess) + self.assertIn('_user_id', sess) self.assertIn('social_auth_last_login_backend', sess) self.assertIn('peam-openidconnect_state', sess) - self.logout() + client.get('/authentication/logout') - with self.app.session_transaction() as sess: + with client.session_transaction() as sess: self.assertIn('this_should_not_be_deleted', sess) - self.assertNotIn('user_id', sess) + self.assertNotIn('_user_id', sess) self.assertNotIn('social_auth_last_login_backend', sess) self.assertNotIn('peam-openidconnect_state', sess) diff --git a/labonneboite/tests/web/front/test_contact_form.py b/labonneboite/tests/web/front/test_contact_form.py index de95b4b99..ebd62e226 100644 --- a/labonneboite/tests/web/front/test_contact_form.py +++ b/labonneboite/tests/web/front/test_contact_form.py @@ -1,3 +1,4 @@ +import pdb from unittest import mock from labonneboite.common import models @@ -75,43 +76,44 @@ def test_update_coordinates_form(self): """ Test `update_coordinates_form` view. """ - with mock.patch('labonneboite.web.contact_form.mail.send_mail'): - - form_data = { - 'new_contact_mode': 'office', - 'new_email': 'exemple@domaine.com', - 'new_email_alternance': 'exemple-alternance@domaine.com', - 'new_phone': '01 77 86 39 49', - 'new_phone_alternance': '02 77 86 39 49', - 'new_website': 'http://exemple.com', - 'social_network': 'https://www.facebook.com/poleemploi/', - 'rgpd_consent': True, - } - form_data.update(self.recruiter_hidden_identification) - - url = self.url_for('contact_form.update_coordinates_form') - rv = self.app.post(url, data=form_data) - - self.assertEqual(rv.status_code, 302) - self.assertIn(self.url_for('contact_form.success'), rv.location) - - # An entry should have been created in `UpdateCoordinatesRecruiterMessage`. - msg = models.UpdateCoordinatesRecruiterMessage.query.filter( - models.UpdateCoordinatesRecruiterMessage.siret == form_data['siret'], - ).one() - self.assertEqual(msg.new_website, form_data['new_website']) - self.assertEqual(msg.new_email, form_data['new_email']) - self.assertEqual(msg.new_phone, form_data['new_phone']) - self.assertEqual(msg.contact_mode, form_data['new_contact_mode']) - self.assertEqual(msg.new_email_alternance, form_data['new_email_alternance']) - self.assertEqual(msg.new_phone_alternance, form_data['new_phone_alternance']) - self.assertEqual(msg.social_network, form_data['social_network']) - self.assertEqual(msg.siret, form_data['siret']) - self.assertEqual(msg.requested_by_first_name, form_data['first_name']) - self.assertEqual(msg.requested_by_last_name, form_data['last_name']) - self.assertEqual(msg.requested_by_email, form_data['email']) - self.assertEqual(msg.requested_by_phone, form_data['phone']) - self.assertEqual(msg.certified_recruiter, False) + with self.login_client.test_client() as client: + with mock.patch('labonneboite.web.contact_form.mail.send_mail'): + + form_data = { + 'new_contact_mode': 'office', + 'new_email': 'exemple@domaine.com', + 'new_email_alternance': 'exemple-alternance@domaine.com', + 'new_phone': '01 77 86 39 49', + 'new_phone_alternance': '02 77 86 39 49', + 'new_website': 'http://exemple.com', + 'social_network': 'https://www.facebook.com/poleemploi/', + 'rgpd_consent': True, + } + form_data.update(self.recruiter_hidden_identification) + + url = self.url_for('contact_form.update_coordinates_form') + rv = client.post(url, data=form_data) + + self.assertEqual(rv.status_code, 302) + self.assertIn(self.url_for('contact_form.success'), rv.location) + + # An entry should have been created in `UpdateCoordinatesRecruiterMessage`. + msg = models.UpdateCoordinatesRecruiterMessage.query.filter( + models.UpdateCoordinatesRecruiterMessage.siret == form_data['siret'], + ).one() + self.assertEqual(msg.new_website, form_data['new_website']) + self.assertEqual(msg.new_email, form_data['new_email']) + self.assertEqual(msg.new_phone, form_data['new_phone']) + self.assertEqual(msg.contact_mode, form_data['new_contact_mode']) + self.assertEqual(msg.new_email_alternance, form_data['new_email_alternance']) + self.assertEqual(msg.new_phone_alternance, form_data['new_phone_alternance']) + self.assertEqual(msg.social_network, form_data['social_network']) + self.assertEqual(msg.siret, form_data['siret']) + self.assertEqual(msg.requested_by_first_name, form_data['first_name']) + self.assertEqual(msg.requested_by_last_name, form_data['last_name']) + self.assertEqual(msg.requested_by_email, form_data['email']) + self.assertEqual(msg.requested_by_phone, form_data['phone']) + self.assertEqual(msg.certified_recruiter, False) class UpdateJobsTest(ContactFormBaseTest): @@ -179,49 +181,50 @@ def test_view_update_jobs_form(self): """ Test `update_jobs_form` view. """ - with mock.patch('labonneboite.web.contact_form.mail.send_mail'): + with self.login_client.test_client() as client: + with mock.patch('labonneboite.web.contact_form.mail.send_mail'): - romes_to_keep = ['D1507', 'D1106'] - romes_alternance_to_keep = ['D1214', 'D1106'] - extra_romes_to_add = ['M1802', 'M1803', 'M1805'] - extra_romes_alternance_to_add = ['M1805'] + romes_to_keep = ['D1507', 'D1106'] + romes_alternance_to_keep = ['D1214', 'D1106'] + extra_romes_to_add = ['M1802', 'M1803', 'M1805'] + extra_romes_alternance_to_add = ['M1805'] - form_data = { - 'romes_to_keep': romes_to_keep, - 'romes_alternance_to_keep': romes_alternance_to_keep, - 'extra_romes_to_add': extra_romes_to_add, - 'extra_romes_alternance_to_add': extra_romes_alternance_to_add, - } - form_data.update(self.recruiter_hidden_identification) + form_data = { + 'romes_to_keep': romes_to_keep, + 'romes_alternance_to_keep': romes_alternance_to_keep, + 'extra_romes_to_add': extra_romes_to_add, + 'extra_romes_alternance_to_add': extra_romes_alternance_to_add, + } + form_data.update(self.recruiter_hidden_identification) - url = self.url_for('contact_form.update_jobs_form') - rv = self.app.post(url, data=form_data) - self.assertEqual(rv.status_code, 302) - self.assertIn(self.url_for('contact_form.success'), rv.location) + url = self.url_for('contact_form.update_jobs_form') + rv = client.post(url, data=form_data) + self.assertEqual(rv.status_code, 302) + self.assertIn(self.url_for('contact_form.success'), rv.location) - # An entry should have been created in `UpdateJobsRecruiterMessage`. - msg = models.UpdateJobsRecruiterMessage.query.filter( - models.UpdateJobsRecruiterMessage.siret == self.office.siret, - ).one() + # An entry should have been created in `UpdateJobsRecruiterMessage`. + msg = models.UpdateJobsRecruiterMessage.query.filter( + models.UpdateJobsRecruiterMessage.siret == self.office.siret, + ).one() - romes_to_add = set(romes_to_keep + extra_romes_to_add) - self.assertCountEqual(romes_to_add, msg.romes_to_add.split(',')) + romes_to_add = set(romes_to_keep + extra_romes_to_add) + self.assertCountEqual(romes_to_add, msg.romes_to_add.split(',')) - romes_alternance_to_add = set(romes_alternance_to_keep + extra_romes_alternance_to_add) - self.assertCountEqual(romes_alternance_to_add, msg.romes_alternance_to_add.split(',')) + romes_alternance_to_add = set(romes_alternance_to_keep + extra_romes_alternance_to_add) + self.assertCountEqual(romes_alternance_to_add, msg.romes_alternance_to_add.split(',')) - romes_to_remove = self.office.romes_codes - romes_to_add - self.assertCountEqual(romes_to_remove, msg.romes_to_remove.split(',')) + romes_to_remove = self.office.romes_codes - romes_to_add + self.assertCountEqual(romes_to_remove, msg.romes_to_remove.split(',')) - romes_alternance_to_remove = self.office.romes_codes - romes_alternance_to_add - self.assertCountEqual(romes_alternance_to_remove, msg.romes_alternance_to_remove.split(',')) + romes_alternance_to_remove = self.office.romes_codes - romes_alternance_to_add + self.assertCountEqual(romes_alternance_to_remove, msg.romes_alternance_to_remove.split(',')) - self.assertEqual(msg.siret, form_data['siret']) - self.assertEqual(msg.requested_by_first_name, form_data['first_name']) - self.assertEqual(msg.requested_by_last_name, form_data['last_name']) - self.assertEqual(msg.requested_by_email, form_data['email']) - self.assertEqual(msg.requested_by_phone, form_data['phone']) - self.assertEqual(msg.certified_recruiter, False) + self.assertEqual(msg.siret, form_data['siret']) + self.assertEqual(msg.requested_by_first_name, form_data['first_name']) + self.assertEqual(msg.requested_by_last_name, form_data['last_name']) + self.assertEqual(msg.requested_by_email, form_data['email']) + self.assertEqual(msg.requested_by_phone, form_data['phone']) + self.assertEqual(msg.certified_recruiter, False) class DeleteFormFormTest(ContactFormBaseTest): @@ -250,32 +253,33 @@ def test_delete_form(self): """ Test `delete_form` view. """ - with mock.patch('labonneboite.web.contact_form.mail.send_mail'): - - form_data = { - 'remove_lba': '1', - 'remove_lbb': '', # Empty means not checked. - } - form_data.update(self.recruiter_hidden_identification) - - url = self.url_for('contact_form.delete_form') - rv = self.app.post(url, data=form_data) - - self.assertEqual(rv.status_code, 302) - self.assertIn(self.url_for('contact_form.success'), rv.location) - - # An entry should have been created in `RemoveRecruiterMessage`. - msg = models.RemoveRecruiterMessage.query.filter( - models.RemoveRecruiterMessage.siret == form_data['siret'], - ).one() - self.assertEqual(msg.remove_lba, 1) - self.assertEqual(msg.remove_lbb, 0) - self.assertEqual(msg.siret, form_data['siret']) - self.assertEqual(msg.requested_by_first_name, form_data['first_name']) - self.assertEqual(msg.requested_by_last_name, form_data['last_name']) - self.assertEqual(msg.requested_by_email, form_data['email']) - self.assertEqual(msg.requested_by_phone, form_data['phone']) - self.assertEqual(msg.certified_recruiter, False) + with self.login_client.test_client() as client: + with mock.patch('labonneboite.web.contact_form.mail.send_mail'): + + form_data = { + 'remove_lba': '1', + 'remove_lbb': '', # Empty means not checked. + } + form_data.update(self.recruiter_hidden_identification) + + url = self.url_for('contact_form.delete_form') + rv = client.post(url, data=form_data) + + self.assertEqual(rv.status_code, 302) + self.assertIn(self.url_for('contact_form.success'), rv.location) + + # An entry should have been created in `RemoveRecruiterMessage`. + msg = models.RemoveRecruiterMessage.query.filter( + models.RemoveRecruiterMessage.siret == form_data['siret'], + ).one() + self.assertEqual(msg.remove_lba, 1) + self.assertEqual(msg.remove_lbb, 0) + self.assertEqual(msg.siret, form_data['siret']) + self.assertEqual(msg.requested_by_first_name, form_data['first_name']) + self.assertEqual(msg.requested_by_last_name, form_data['last_name']) + self.assertEqual(msg.requested_by_email, form_data['email']) + self.assertEqual(msg.requested_by_phone, form_data['phone']) + self.assertEqual(msg.certified_recruiter, False) class OtherFormTest(ContactFormBaseTest): @@ -309,28 +313,29 @@ def test_update_coordinates_form(self): """ Test `other_form` view. """ - with mock.patch('labonneboite.web.contact_form.mail.send_mail'): - - form_data = { - 'comment': 'Bonjour à tous', - } - form_data.update(self.recruiter_hidden_identification) - - url = self.url_for('contact_form.other_form') - rv = self.app.post(url, data=form_data) - - self.assertEqual(rv.status_code, 302) - self.assertIn(self.url_for('contact_form.success'), rv.location) - - # An entry should have been created in `OtherRecruiterMessage`. - msg = models.OtherRecruiterMessage.query.filter( - models.OtherRecruiterMessage.siret == form_data['siret'], - ).one() - - self.assertEqual(msg.comment, form_data['comment']) - self.assertEqual(msg.siret, form_data['siret']) - self.assertEqual(msg.requested_by_first_name, form_data['first_name']) - self.assertEqual(msg.requested_by_last_name, form_data['last_name']) - self.assertEqual(msg.requested_by_email, form_data['email']) - self.assertEqual(msg.requested_by_phone, form_data['phone']) - self.assertEqual(msg.certified_recruiter, False) + with self.login_client.test_client() as client: + with mock.patch('labonneboite.web.contact_form.mail.send_mail'): + + form_data = { + 'comment': 'Bonjour à tous', + } + form_data.update(self.recruiter_hidden_identification) + + url = self.url_for('contact_form.other_form') + rv = client.post(url, data=form_data) + + self.assertEqual(rv.status_code, 302) + self.assertIn(self.url_for('contact_form.success'), rv.location) + + # An entry should have been created in `OtherRecruiterMessage`. + msg = models.OtherRecruiterMessage.query.filter( + models.OtherRecruiterMessage.siret == form_data['siret'], + ).one() + + self.assertEqual(msg.comment, form_data['comment']) + self.assertEqual(msg.siret, form_data['siret']) + self.assertEqual(msg.requested_by_first_name, form_data['first_name']) + self.assertEqual(msg.requested_by_last_name, form_data['last_name']) + self.assertEqual(msg.requested_by_email, form_data['email']) + self.assertEqual(msg.requested_by_phone, form_data['phone']) + self.assertEqual(msg.certified_recruiter, False) diff --git a/labonneboite/tests/web/front/test_favorites.py b/labonneboite/tests/web/front/test_favorites.py index 01526c836..9d6193f7a 100644 --- a/labonneboite/tests/web/front/test_favorites.py +++ b/labonneboite/tests/web/front/test_favorites.py @@ -139,15 +139,12 @@ def test_favorites_download(self): rv = self.app.get(url) self.assertEqual(rv.status_code, 401) - with self.test_request_context(): - - self.login(self.user) - + with self.login_client.test_client(user=self.user) as client: # Create a favorite for the user. UserFavoriteOffice.create(user_id=self.user.id, office_siret=office.siret) - rv = self.app.get(url) + rv = client.get(url) self.assertEqual(rv.status_code, 200) self.assertEqual('application/csv', rv.mimetype) self.assertIn('siret', rv.data.decode('utf-8')) @@ -164,11 +161,9 @@ def test_favorites_list(self): rv = self.app.get(url_list) self.assertEqual(rv.status_code, 401) - with self.test_request_context(): + with self.login_client.test_client(user=self.user) as client: - self.login(self.user) - - rv = self.app.get(url_list) + rv = client.get(url_list) self.assertEqual(rv.status_code, 200) self.assertTrue( 'Aucun favori pour le moment.' in rv.data.decode('utf-8')) @@ -177,7 +172,7 @@ def test_favorites_list(self): UserFavoriteOffice.create(user_id=self.user.id, office_siret=office.siret) - rv = self.app.get(url_list) + rv = client.get(url_list) self.assertEqual(rv.status_code, 200) self.assertTrue(office.name in rv.data.decode('utf-8')) self.assertTrue(office.city in rv.data.decode('utf-8')) @@ -188,38 +183,37 @@ def test_favorites_add(self): """ rome_code = 'M1805' office = Office.query.filter(Office.siret == '00000000000002').one() - url_list = self.url_for('user.favorites_list') + url_list = self.url_for('user.favorites_list', _external=False) url_add = self.url_for('user.favorites_add', siret=office.siret, - rome_code=rome_code) + rome_code=rome_code, _external=False) url_search_without_domain = '/entreprises/nancy-54100/strategie-commerciale' - url_search_with_domain = 'http://labonneboite.pole-emploi.fr' + url_search_without_domain # An anonymous user cannot add a favorite. - rv = self.app.post(url_add) - self.assertEqual(rv.status_code, 401) + with self.login_client.test_client() as client: - with self.test_request_context(): + rv = client.post(url_add) + self.assertEqual(rv.status_code, 401) - self.login(self.user) + with self.login_client.test_client(user=self.user) as client: - rv = self.app.get(url_list) + rv = client.get(url_list) self.assertEqual(rv.status_code, 200) self.assertTrue( 'Aucun favori pour le moment.' in rv.data.decode('utf-8')) # Adding favorite without next_url : # User should be redirected to the favorites list by default. - rv = self.app.post(url_add) + rv = client.post(url_add) self.assertEqual(rv.status_code, 302) self.assertEqual(rv.location, url_list) # Adding favorite from search results - the realistic case. # User should be redirected back to the search results. - rv = self.app.post(url_add, + rv = client.post(url_add, data={'next': url_search_without_domain}) self.assertEqual(rv.status_code, 302) - self.assertEqual(rv.location, url_search_with_domain) + self.assertEqual(rv.location, url_search_without_domain) favorites = UserFavoriteOffice.query.filter( UserFavoriteOffice.user_id == self.user.id).all() @@ -227,7 +221,7 @@ def test_favorites_add(self): self.assertEqual(office.siret, favorites[0].office_siret) self.assertEqual(rome_code, favorites[0].rome_code) - rv = self.app.get(url_list) + rv = client.get(url_list) self.assertEqual(rv.status_code, 200) self.assertTrue(office.name in rv.data.decode('utf-8')) self.assertTrue(office.city in rv.data.decode('utf-8')) @@ -249,10 +243,9 @@ def test_favorites_add_without_rome_code(self): self.assertEqual(0, UserFavoriteOffice.query.filter( UserFavoriteOffice.user_id == self.user.id).count()) - with self.test_request_context(): - self.login(self.user) + with self.login_client.test_client(user=self.user) as client: - rv = self.app.post(url_add) + rv = client.post(url_add) self.assertEqual(rv.status_code, 302) favorites = UserFavoriteOffice.query.filter( @@ -266,31 +259,28 @@ def test_favorites_delete(self): Test the deletion of a favorite. """ office = Office.query.filter(Office.siret == '00000000000003').one() - url_list = self.url_for('user.favorites_list') - url_delete = self.url_for('user.favorites_delete', siret=office.siret) + url_list = self.url_for('user.favorites_list', _external=False) + url_delete = self.url_for('user.favorites_delete', siret=office.siret, _external=False) url_search_without_domain = '/entreprises/nancy-54100/strategie-commerciale' - url_search_with_domain = 'http://labonneboite.pole-emploi.fr' + url_search_without_domain # An anonymous user cannot delete a favorite. rv = self.app.post(url_delete) self.assertEqual(rv.status_code, 401) - with self.test_request_context(): - - self.login(self.user) + with self.login_client.test_client(user=self.user) as client: # Create a favorite for the user. UserFavoriteOffice.create(user_id=self.user.id, office_siret=office.siret) - rv = self.app.get(url_list) + rv = client.get(url_list) self.assertEqual(rv.status_code, 200) self.assertTrue(office.name in rv.data.decode('utf-8')) self.assertTrue(office.city in rv.data.decode('utf-8')) # Deleting favorite without next_url : # User should be redirected to the favorites list by default. - rv = self.app.post(url_delete) + rv = client.post(url_delete) self.assertEqual(rv.status_code, 302) self.assertEqual(rv.location, url_list) @@ -300,12 +290,12 @@ def test_favorites_delete(self): # Deleting favorite from search results - the realistic case. # User should be redirected back to the search results. - rv = self.app.post(url_delete, + rv = client.post(url_delete, data={'next': url_search_without_domain}) self.assertEqual(rv.status_code, 302) - self.assertEqual(rv.location, url_search_with_domain) + self.assertEqual(rv.location, url_search_without_domain) - rv = self.app.get(url_list) + rv = client.get(url_list) self.assertEqual(rv.status_code, 200) self.assertTrue( 'Aucun favori pour le moment.' in rv.data.decode('utf-8')) @@ -316,9 +306,8 @@ def test_favorites_download_list_as_pdf(self): UserFavoriteOffice.create(user_id=self.user.id, office_siret=office.siret) - with self.test_request_context(): - self.login(self.user) - rv = self.app.get(url_favorites_download) + with self.login_client.test_client(user=self.user) as client: + rv = client.get(url_favorites_download) self.assertEqual(rv.status_code, 200) self.assertEqual('application/pdf', rv.mimetype) diff --git a/labonneboite/tests/web/front/test_pro_version.py b/labonneboite/tests/web/front/test_pro_version.py index 60f759db4..5f395fc5a 100644 --- a/labonneboite/tests/web/front/test_pro_version.py +++ b/labonneboite/tests/web/front/test_pro_version.py @@ -1,14 +1,12 @@ import ipaddress -from unittest import mock -from flask import current_app +import pytest from labonneboite.common import pro from labonneboite.common.models import User from labonneboite.tests.test_base import DatabaseTest from labonneboite.conf import settings - class ProVersionTest(DatabaseTest): def setUp(self): @@ -32,27 +30,29 @@ def setUp(self): 'User_Agent': user_agent, } - def test_user_is_pro(self): """ Test that the Pro user is correctly detected in various cases. """ + url = self.url_for('user.pro_version') + # Email detection, without IP detection with self.test_request_context(): + # User which is not logged in should not be considered a pro user. self.assertFalse(pro.user_is_pro()) + with self.login_client.test_client(user=self.pro_user) as client: + client.get(url) + # # User with a pro email should be considered as a pro user. - self.login(self.pro_user) self.assertTrue(pro.user_is_pro()) - self.logout() - self.assertFalse(pro.user_is_pro()) + + with self.login_client.test_client(user=self.public_user) as client: + client.get(url) # User with a non pro email should not be considered a pro user. - self.login(self.public_user) - self.assertFalse(pro.user_is_pro()) - self.logout() self.assertFalse(pro.user_is_pro()) # a public user logging in with the right IP address @@ -60,48 +60,47 @@ def test_user_is_pro(self): with self.test_request_context(headers=self.headers): self.assertTrue(pro.user_is_pro()) - def test_enable_disable_pro_version_view(self): """ Test that the Pro Version is correctly enabled/disabled. """ next_url_without_domain = '/entreprises/metz-57000/boucherie?sort=score&d=10&h=1&p=0&f_a=0' - next_url_with_domain = 'http://labonneboite.pole-emploi.fr' + next_url_without_domain url = self.url_for('user.pro_version', **{'next': next_url_without_domain}) with self.test_request_context(headers=self.headers): - # Log the user in. - self.login(self.pro_user) - self.assertTrue(pro.user_is_pro()) - self.assertFalse(pro.pro_version_enabled()) - - with self.app.session_transaction() as sess: - self.assertNotIn(pro.PRO_VERSION_SESSION_KEY, sess) - - # Enable pro version. - rv = self.app.get(url) - self.assertEqual(rv.status_code, 302) - self.assertEqual(rv.location, next_url_with_domain) - # It is unclear what is the root cause of the following test - # failure. The session object manipulated in the - # pro_version_enabled function is different from the session - # provided by the self.app.session_transaction context manager, but - # I don't know why. - # self.assertTrue(pro.pro_version_enabled()) - - with self.app.session_transaction() as sess: - self.assertIn(pro.PRO_VERSION_SESSION_KEY, sess) - self.assertEqual(True, sess[pro.PRO_VERSION_SESSION_KEY]) - - # Disable pro version. - rv = self.app.get(url) - self.assertEqual(rv.status_code, 302) - self.assertEqual(rv.location, next_url_with_domain) - self.assertFalse(pro.pro_version_enabled()) - - with self.app.session_transaction() as sess: - self.assertNotIn(pro.PRO_VERSION_SESSION_KEY, sess) + with self.login_client.test_client(user=self.pro_user) as client: + + # Log the user in. + self.assertTrue(pro.user_is_pro()) + self.assertFalse(pro.pro_version_enabled()) + + with client.session_transaction() as sess: + self.assertNotIn(pro.PRO_VERSION_SESSION_KEY, sess) + + # Enable pro version. + rv = client.get(url) + self.assertEqual(rv.status_code, 302) + self.assertEqual(rv.location, next_url_without_domain) + # It is unclear what is the root cause of the following test + # failure. The session object manipulated in the + # pro_version_enabled function is different from the session + # provided by the self.app.session_transaction context manager, but + # I don't know why. + # self.assertTrue(pro.pro_version_enabled()) + + with client.session_transaction() as sess: + self.assertIn(pro.PRO_VERSION_SESSION_KEY, sess) + self.assertEqual(True, sess[pro.PRO_VERSION_SESSION_KEY]) + + # Disable pro version. + rv = client.get(url) + self.assertEqual(rv.status_code, 302) + self.assertEqual(rv.location, next_url_without_domain) + self.assertFalse(pro.pro_version_enabled()) + + with client.session_transaction() as sess: + self.assertNotIn(pro.PRO_VERSION_SESSION_KEY, sess) def test_pro_version_in_a_pila_machine(self): diff --git a/labonneboite/tests/web/front/test_root.py b/labonneboite/tests/web/front/test_root.py index 7a17eacff..06650440b 100644 --- a/labonneboite/tests/web/front/test_root.py +++ b/labonneboite/tests/web/front/test_root.py @@ -4,13 +4,19 @@ from labonneboite.common import pro from labonneboite.common.models import User -class RootTest(DatabaseTest): - def login_as_pro(self): - user_pro = User.create(email='x@pole-emploi.fr', gender='male', first_name='John', last_name='Doe') - self.login(user_pro) - self.assertTrue(pro.user_is_pro()) - self.assertFalse(pro.pro_version_enabled()) +class RootBaseTest(DatabaseTest): + def setUp(self): + super(RootBaseTest, self).setUp() + + # Create a user. + self.pro_user = User.create(email='x@pole-emploi.fr', + gender='male', + first_name='John', + last_name='Doe') + + +class RootTest(RootBaseTest): def test_no_kit_if_public_user(self): rv = self.app.get(self.url_for('root.kit')) @@ -18,23 +24,21 @@ def test_no_kit_if_public_user(self): @mock.patch('labonneboite.conf.settings.VERSION_PRO_ALLOWED_EMAIL_SUFFIXES', ['@pole-emploi.fr']) def test_no_kit_if_pro_but_not_enabled(self): - with self.test_request_context(): - self.login_as_pro() + with self.login_client.test_client(user=self.pro_user) as client: - rv = self.app.get(self.url_for('root.kit')) + rv = client.get(self.url_for('root.kit')) self.assertEqual(rv.status_code, 404) @mock.patch('labonneboite.conf.settings.VERSION_PRO_ALLOWED_EMAIL_SUFFIXES', ['@pole-emploi.fr']) def test_kit_if_pro_and_enabled(self): - with self.test_request_context(): - self.login_as_pro() + with self.login_client.test_client(user=self.pro_user) as client: # enable pro version - with self.app.session_transaction() as sess: + with client.session_transaction() as sess: sess[pro.PRO_VERSION_SESSION_KEY] = True # Non-empty kit page - rv = self.app.get(self.url_for('root.kit')) + rv = client.get(self.url_for('root.kit')) self.assertEqual(rv.status_code, 200) self.assertEqual('text/html; charset=utf-8', rv.content_type) self.assertLess(1000, rv.content_length) diff --git a/labonneboite/tests/web/front/test_routes.py b/labonneboite/tests/web/front/test_routes.py index 14993276f..bfe9955e9 100644 --- a/labonneboite/tests/web/front/test_routes.py +++ b/labonneboite/tests/web/front/test_routes.py @@ -206,7 +206,7 @@ def test_generic_url_search_by_commune_and_rome(self): url, querystring = urllib.parse.splitquery(rv.location) parameters = dict(parse_qsl(querystring)) - expected_url = self.url_for('search.entreprises') + expected_url = self.url_for('search.entreprises', _external=False) expected_parameters = { 'city': 'paris', 'zipcode': '75000', @@ -224,7 +224,7 @@ def test_generic_url_search_by_commune_and_rome_with_distance(self): url, querystring = urllib.parse.splitquery(rv.location) parameters = dict(parse_qsl(querystring)) - expected_url = self.url_for('search.entreprises') + expected_url = self.url_for('search.entreprises', _external=False) expected_parameters = { 'city': 'paris', 'zipcode': '75000', @@ -242,7 +242,7 @@ def test_generic_url_search_by_commune_and_rome_with_utm_campaign(self): rv = self.app.get(url) self.assertEqual(rv.status_code, 302) - expected_url = self.url_for('search.entreprises') + expected_url = self.url_for('search.entreprises', _external=False) expected_parameters = { 'city': 'paris', 'zipcode': '75000', @@ -260,7 +260,7 @@ def test_get_url_for_rome_departments(self): with self.app_context(): with mock.patch('labonneboite.common.geocoding.datagouv.get_department_by_code', return_value={"department": "57", "label": 'Moselle (57)'}): url = get_url_for_rome('M1805', '57') - self.assertEqual(url, 'http://labonneboite.pole-emploi.fr/entreprises?departments=57&j=M1805&l=Moselle+%2857%29&occupation=etudes-et-developpement-informatique') + self.assertEqual(url, 'http://labonneboite.pole-emploi.fr/entreprises?departments=57&j=M1805&l=Moselle+(57)&occupation=etudes-et-developpement-informatique') with mock.patch('labonneboite.common.geocoding.datagouv.get_department_by_code', return_value=None): url = get_url_for_rome('M1805', '57') self.assertEqual(url, None) diff --git a/labonneboite/tests/web/front/test_user_account.py b/labonneboite/tests/web/front/test_user_account.py index a993450bc..a7b01fbaa 100644 --- a/labonneboite/tests/web/front/test_user_account.py +++ b/labonneboite/tests/web/front/test_user_account.py @@ -74,12 +74,10 @@ def test_download_user_personal_data(self): url = self.url_for('user.personal_data_as_csv') - with self.test_request_context(): - - self.login(self.user) + with self.login_client.test_client(user=self.user) as client: # Display the account deletion confirmation page. - rv = self.app.get(url) + rv = client.get(url) self.assertEqual(rv.status_code, 200) self.assertIn('john@doe.com', rv.data.decode('utf-8')) @@ -91,16 +89,19 @@ def test_delete_user_account(self): url = self.url_for('user.account_delete') - with self.test_request_context(): - - self.login(self.user) + with self.login_client.test_client(user=self.user) as client: # Display the account deletion confirmation page. - rv = self.app.get(url) + with client.session_transaction() as sess: + # Session info set by Python Social Auth. + sess['social_auth_last_login_backend'] = 'peam-openidconnect' + sess['%s_state' % 'peam-openidconnect'] = 'a1z2e3r4t5y6y' + + rv = client.get(url) self.assertEqual(rv.status_code, 200) # Confirm account deletion. - rv = self.app.post(url, data={'confirm_deletion': 1}) + rv = client.post(url, data={'confirm_deletion': 1}) # The user should be redirected to the PEAM logout endpoint. self.assertEqual(rv.status_code, 302) self.assertIn(settings.PEAM_AUTH_BASE_URL, rv.location) @@ -112,5 +113,5 @@ def test_delete_user_account(self): self.assertEqual(db_session.query(UserSocialAuth).count(), 0) # The user should now be anonymous and cannot access protected pages. - rv = self.app.get(url) + rv = client.get(url) self.assertEqual(rv.status_code, 401) diff --git a/labonneboite/web/admin/views/office_admin_add.py b/labonneboite/web/admin/views/office_admin_add.py index 9cdeade8b..ff68a1d7d 100644 --- a/labonneboite/web/admin/views/office_admin_add.py +++ b/labonneboite/web/admin/views/office_admin_add.py @@ -204,11 +204,11 @@ class OfficeAdminAddModelView(AdminModelViewMixin, ModelView): # type: ignore }, "x": { "filters": [strip_filter, nospace_filter], - "validators": [DataRequired()], # sytt: instead of [validators.required()] + "validators": [DataRequired()], }, "y": { "filters": [strip_filter, nospace_filter], - "validators": [DataRequired()], # sytt: instead of [validators.required()] + "validators": [DataRequired()], }, } diff --git a/labonneboite/web/api/util.py b/labonneboite/web/api/util.py index 32185dfa8..f63fc501f 100644 --- a/labonneboite/web/api/util.py +++ b/labonneboite/web/api/util.py @@ -1,6 +1,7 @@ import datetime import hmac import urllib.request, urllib.parse, urllib.error +import hashlib from labonneboite.conf import settings class TimestampFormatException(Exception): @@ -59,7 +60,7 @@ def check_api_request(request): def compute_signature(args, api_key): ordered_arg_string = get_ordered_argument_string(args) - return hmac.new(api_key.encode(), ordered_arg_string.encode()).hexdigest() + return hmac.new(api_key.encode(), ordered_arg_string.encode(), hashlib.sha256).hexdigest() def check_signature(request, requested_signature, api_key): diff --git a/labonneboite/web/contact_form/forms.py b/labonneboite/web/contact_form/forms.py index 9ac1d38a7..9b20d11f1 100644 --- a/labonneboite/web/contact_form/forms.py +++ b/labonneboite/web/contact_form/forms.py @@ -1,24 +1,12 @@ from flask import request - from flask_wtf import FlaskForm -from wtforms import ( - BooleanField, - HiddenField, - RadioField, - SelectMultipleField, - StringField, - TextAreaField, -) -from wtforms import validators # sytt : this is import if it works, it's a fluke... - -# from wtforms.fields import EmailField, TelField # compatibility 3.10 : wtfform > 3.0.0 +from wtforms import BooleanField, HiddenField, RadioField, SelectMultipleField, StringField, TextAreaField, validators from wtforms.fields.html5 import EmailField, TelField from wtforms.validators import DataRequired, Email, Optional, Regexp, URL -from wtforms.widgets import ListWidget, CheckboxInput +from wtforms.widgets import CheckboxInput, ListWidget from labonneboite.conf import settings - PHONE_REGEX = r"^(0|\+33)[1-9]([-. ]?[0-9]{2}){4}$" SIRET_REGEX = r"[0-9]{14}" @@ -211,8 +199,13 @@ class OfficeUpdateCoordinatesForm(OfficeHiddenIdentificationForm): render_kw={"placeholder": "01 77 86 39 49, +331 77 86 39 49"}, ) rgpd_consent = BooleanField( +<<<<<<< HEAD "En cochant cette case, vous consentez à diffuser des données à caractère personnel sur les services numériques de Pôle emploi.", validators=[DataRequired()], # sytt: instead of [validators.required()] +======= + 'En cochant cette case, vous consentez à diffuser des données à caractère personnel sur les services numériques de Pôle emploi.', + [validators.InputRequired()] +>>>>>>> 8e61dc28... :recycle: upgrade python to v3.10 ) diff --git a/labonneboite/web/office/views.py b/labonneboite/web/office/views.py index 7f9d01581..27e00c689 100644 --- a/labonneboite/web/office/views.py +++ b/labonneboite/web/office/views.py @@ -71,7 +71,7 @@ def download(siret): attachment_name = 'fiche_entreprise_%s.pdf' % slugify(office.name, separator='_') pdf_path = office_detail_pdf_path(office) - return send_file(pdf_path, mimetype='application/pdf', as_attachment=True, attachment_filename=attachment_name) + return send_file(pdf_path, mimetype='application/pdf', as_attachment=True, download_name=attachment_name) @officeBlueprint.route('//download.html') diff --git a/labonneboite/web/templates_functions.py b/labonneboite/web/templates_functions.py index 82e04ff1b..12ead86dd 100644 --- a/labonneboite/web/templates_functions.py +++ b/labonneboite/web/templates_functions.py @@ -16,7 +16,7 @@ def get_stars_for_rome_code(self, _=None): def include_file(flask_app: Flask): def include_file(filename): - return jinja2.Markup.escape(flask_app.jinja_loader.get_source(flask_app.jinja_env, filename)[0]) + return jinja2.utils.markupsafe.Markup.escape(flask_app.jinja_loader.get_source(flask_app.jinja_env, filename)[0]) return include_file diff --git a/labonneboite/web/user/views.py b/labonneboite/web/user/views.py index e34449e00..2e911e473 100644 --- a/labonneboite/web/user/views.py +++ b/labonneboite/web/user/views.py @@ -133,8 +133,8 @@ def favorites_list_as_pdf(): return send_file(pdf_file, mimetype='application/pdf', as_attachment=True, - attachment_filename='mes_favoris.pdf', - cache_timeout=5) + download_name='mes_favoris.pdf', + max_age=5) def make_csv_response(csv_text, attachment_name): diff --git a/requirements.dev.in b/requirements.dev.in index 7f858eafd..5ff862a2e 100644 --- a/requirements.dev.in +++ b/requirements.dev.in @@ -17,6 +17,7 @@ ipython pylint # testing +easyprocess # locust ########## diff --git a/requirements.dev.txt b/requirements.dev.txt index 9407a955b..44757df92 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -1,160 +1,784 @@ # -# This file is autogenerated by pip-compile +# This file is autogenerated by pip-compile with python 3.10 # To update, run: # -# pip-compile --output-file requirements.dev.txt requirements.dev.in +# pip-compile --output-file=requirements.dev.txt requirements.dev.in # -e git+https://github.com/StartupsPoleEmploi/labonneboite-common.git@f0dadcbb79338522169e586a9f4ec0a750920d00#egg=labonneboite-common -alembic==0.9.10 -astroid==1.6.5 -attrs==22.1.0 # via flake8-mypy, pytest -babel==2.6.0 -backcall==0.1.0 -blinker==1.4 -cachetools==4.0.0 -certifi==2017.4.17 -cffi==1.13.1 -chardet==3.0.4 -click==8.0.4 -coverage==6.2 -cryptography==2.8 + # via -r requirements.txt +-e git+https://github.com/python-social-auth/social-storage-sqlalchemy.git@b489c10244a45ae889c554bddc6ab69a7a7e5057#egg=social-auth-storage-sqlalchemy + # via + # -r requirements.txt + # social-auth-app-flask-sqlalchemy +-e git+https://github.com/zopefoundation/zope.event.git@cdd8d1976ef48d4a59ba49d57d4c956456640c1c#egg=zope.event + # via + # -r requirements.txt + # gevent +-e git+https://github.com/zopefoundation/zope.interface.git@24bd6eefaecf2195977a139cdaff2ccfef4e85cd#egg=zope.interface + # via + # -r requirements.txt + # gevent +alembic==1.8.1 + # via -r requirements.txt +arabic-reshaper==2.1.3 + # via + # -r requirements.txt + # xhtml2pdf +asn1crypto==1.5.1 + # via + # -r requirements.txt + # oscrypto + # pyhanko + # pyhanko-certvalidator +astroid==2.12.5 + # via + # -r requirements.txt + # pylint +asttokens==2.0.8 + # via + # -r requirements.txt + # stack-data +async-generator==1.10 + # via + # -r requirements.txt + # trio + # trio-websocket +attrs==22.1.0 + # via + # -r requirements.txt + # flake8-mypy + # outcome + # pytest + # trio +babel==2.10.3 + # via + # -r requirements.txt + # flask-babelex +backcall==0.2.0 + # via + # -r requirements.txt + # ipython +blinker==1.5 + # via + # -r requirements.txt + # flask-debugtoolbar + # raven +brotli==1.0.9 + # via + # -r requirements.txt + # geventhttpclient +build==0.8.0 + # via + # -r requirements.txt + # pip-tools +cachetools==5.2.0 + # via + # -r requirements.txt + # google-auth +certifi==2022.6.15 + # via + # -r requirements.txt + # geventhttpclient + # requests + # selenium + # sentry-sdk +cffi==1.15.1 + # via + # -r requirements.txt + # cryptography +charset-normalizer==2.1.1 + # via + # -r requirements.txt + # requests +click==8.1.3 + # via + # -r requirements.txt + # flask + # pip-tools + # pyhanko +configargparse==1.5.3 + # via + # -r requirements.txt + # locust +coverage==6.4.4 + # via -r requirements.dev.in +cryptography==37.0.4 + # via + # -r requirements.txt + # pyhanko + # pyhanko-certvalidator + # social-auth-core cssmin==0.2.0 -dataclasses==0.8 -decorator==4.3.0 -defusedxml==0.5.0 -easyprocess==0.3 -elasticsearch-stubs==0 + # via -r requirements.txt +cssselect2==0.6.0 + # via + # -r requirements.txt + # svglib +decorator==5.1.1 + # via + # -r requirements.txt + # ipdb + # ipython + # validators +defusedxml==0.7.1 + # via + # -r requirements.txt + # python3-openid + # social-auth-core +dill==0.3.5.1 + # via + # -r requirements.txt + # pylint +dnspython==2.2.1 + # via + # -r requirements.txt + # email-validator +easyprocess==1.1 + # via -r requirements.dev.in +ecdsa==0.18.0 + # via + # -r requirements.txt + # python-jose elasticsearch==1.9.0 -first==2.0.1 + # via -r requirements.txt +elasticsearch-stubs==0 + # via -r requirements.dev.in +email-validator==1.2.1 + # via -r requirements.txt +executing==1.0.0 + # via + # -r requirements.txt + # stack-data +flake8==5.0.4 + # via + # -r requirements.dev.in + # flake8-mypy flake8-mypy==17.8.0 -flake8==4.0.0 + # via -r requirements.dev.in +flask==2.2.2 + # via + # -r requirements.txt + # flask-admin + # flask-assets + # flask-babelex + # flask-basicauth + # flask-cors + # flask-debugtoolbar + # flask-login + # flask-script + # flask-testing + # flask-wtf + # locust + # raven flask-admin==1.6.0 -flask-assets==0.12 + # via -r requirements.txt +flask-assets==2.0 + # via -r requirements.txt flask-babelex==0.9.4 + # via -r requirements.txt +flask-basicauth==0.2.0 + # via + # -r requirements.txt + # locust flask-cors==3.0.10 + # via + # -r requirements.txt + # locust flask-debugtoolbar==0.13.1 -flask-login==0.4.1 + # via + # -r requirements.dev.in + # -r requirements.txt +flask-login==0.6.2 + # via + # -r requirements.txt + # social-auth-app-flask flask-script==2.0.6 + # via -r requirements.txt flask-testing==0.8.1 + # via -r requirements.txt flask-wtf==1.0.1 -flask==2.0.3 -future==0.16.0 -geographiclib==1.49 -geopy==1.19.0 -gevent==1.1.1 -google-api-python-client==1.7.11 -google-auth-httplib2==0.0.3 -google-auth-oauthlib==0.4.1 -google-auth==1.11.0 -greenlet==0.4.12 -html5lib==1.0.1 -httplib2==0.11.3 -idna==2.5 -importlib-metadata==4.2.0 -iniconfig==1.1.1 # via pytest + # via -r requirements.txt +future==0.18.2 + # via + # -r requirements.txt + # arabic-reshaper +geographiclib==1.52 + # via + # -r requirements.txt + # geopy +geopy==2.2.0 + # via -r requirements.txt +gevent==21.12.0 + # via + # -r requirements.txt + # geventhttpclient + # locust +geventhttpclient==2.0.2 + # via + # -r requirements.txt + # locust +google-api-core==2.10.0 + # via + # -r requirements.txt + # google-api-python-client +google-api-python-client==2.58.0 + # via -r requirements.txt +google-auth==2.11.0 + # via + # -r requirements.txt + # google-api-core + # google-api-python-client + # google-auth-httplib2 + # google-auth-oauthlib +google-auth-httplib2==0.1.0 + # via + # -r requirements.txt + # google-api-python-client +google-auth-oauthlib==0.5.2 + # via -r requirements.txt +googleapis-common-protos==1.56.4 + # via + # -r requirements.txt + # google-api-core +greenlet==1.1.3 + # via + # -r requirements.txt + # gevent +h11==0.13.0 + # via + # -r requirements.txt + # wsproto +html5lib==1.1 + # via + # -r requirements.txt + # xhtml2pdf +httplib2==0.20.4 + # via + # -r requirements.txt + # google-api-python-client + # google-auth-httplib2 +idna==3.3 + # via + # -r requirements.txt + # email-validator + # requests + # trio +iniconfig==1.1.1 + # via pytest ipdb==0.13.9 -ipython-genutils==0.2.0 -ipython==7.16.1 -isort==4.2.15 -itsdangerous==2.0.1 -jedi==0.12.0 -jinja2==3.0.3 -jsmin==3.0.0 -lazy-object-proxy==1.3.1 -line-profiler==2.0 -locustio==0.7.5 -mailjet-rest==1.3.3 -mako==1.0.7 -markupsafe==2.0.1 -mccabe==0.6.1 -msgpack-python==0.5.6 -mypy-extensions==0.4.3 # via mypy + # via + # -r requirements.dev.in + # -r requirements.txt +ipython==8.4.0 + # via + # -r requirements.dev.in + # -r requirements.txt + # ipdb +isort==5.10.1 + # via + # -r requirements.dev.in + # -r requirements.txt + # pylint +itsdangerous==2.1.2 + # via + # -r requirements.txt + # flask + # flask-debugtoolbar + # flask-wtf +jedi==0.18.1 + # via + # -r requirements.txt + # ipython +jinja2==3.1.2 + # via + # -r requirements.txt + # flask + # flask-babelex +jsmin==3.0.1 + # via -r requirements.txt +lazy-object-proxy==1.7.1 + # via + # -r requirements.txt + # astroid +line-profiler==3.5.1 + # via -r requirements.txt +locust==2.11.1 + # via -r requirements.txt +lxml==4.9.1 + # via + # -r requirements.txt + # svglib +mailjet-rest==1.3.4 + # via -r requirements.txt +mako==1.2.2 + # via + # -r requirements.txt + # alembic +markupsafe==2.1.1 + # via + # -r requirements.txt + # jinja2 + # mako + # werkzeug + # wtforms +matplotlib-inline==0.1.6 + # via + # -r requirements.txt + # ipython +mccabe==0.7.0 + # via + # -r requirements.txt + # flake8 + # pylint +msgpack==1.0.4 + # via + # -r requirements.txt + # locust mypy==0.971 -mysqlclient==1.4.2.post1 -nose==1.3.7 -numpy==1.16.1 -oauthlib==2.0.2 -packaging==21.3 # via pytest -pandas==0.22.0 -parameterized==0.7.0 -parso==0.2.1 -pexpect==4.6.0 -pickleshare==0.7.4 -pillow==6.0.0 -pip-tools==2.0.2 -pluggy==1.0.0 # via pytest -prompt-toolkit==3.0.21 -ptyprocess==0.6.0 -py==1.11.0 # via pytest -pyasn1-modules==0.2.8 + # via + # -r requirements.dev.in + # flake8-mypy + # sqlalchemy-stubs +mypy-extensions==0.4.3 + # via mypy +mysqlclient==2.1.1 + # via -r requirements.txt +numpy==1.23.2 + # via + # -r requirements.txt + # pandas +oauthlib==3.2.0 + # via + # -r requirements.txt + # requests-oauthlib + # social-auth-core +oscrypto==1.3.0 + # via + # -r requirements.txt + # pyhanko-certvalidator +outcome==1.2.0 + # via + # -r requirements.txt + # trio +packaging==21.3 + # via + # -r requirements.txt + # build + # pytest +pandas==1.4.4 + # via -r requirements.txt +pandas-stubs==1.4.4.220906 + # via -r requirements.dev.in +parameterized==0.8.1 + # via -r requirements.txt +parso==0.8.3 + # via + # -r requirements.txt + # jedi +pep517==0.13.0 + # via + # -r requirements.txt + # build +pexpect==4.8.0 + # via + # -r requirements.txt + # ipython +pickleshare==0.7.5 + # via + # -r requirements.txt + # ipython +pillow==9.2.0 + # via + # -r requirements.txt + # reportlab + # xhtml2pdf +pip-tools==6.8.0 + # via -r requirements.txt +platformdirs==2.5.2 + # via + # -r requirements.txt + # pylint +pluggy==1.0.0 + # via pytest +prompt-toolkit==3.0.31 + # via + # -r requirements.txt + # ipython +protobuf==4.21.5 + # via + # -r requirements.txt + # google-api-core + # googleapis-common-protos +psutil==5.9.2 + # via + # -r requirements.txt + # locust +ptyprocess==0.7.0 + # via + # -r requirements.txt + # pexpect +pure-eval==0.2.2 + # via + # -r requirements.txt + # stack-data +py==1.11.0 + # via pytest pyasn1==0.4.8 -pycodestyle==2.8.0 # via flake8 -pycparser==2.19 -pycryptodomex==3.6.3 -pyflakes==2.4.0 # via flake8 -pygments==2.2.0 -pyjwkest==1.4.0 -pyjwt==1.5.2 -pylint==1.9.2 -pyparsing==3.0.9 # via packaging -pypdf2==1.26.0 -pyprof2calltree==1.4.3 + # via + # -r requirements.txt + # pyasn1-modules + # python-jose + # rsa +pyasn1-modules==0.2.8 + # via + # -r requirements.txt + # google-auth +pycodestyle==2.9.1 + # via flake8 +pycparser==2.21 + # via + # -r requirements.txt + # cffi +pyflakes==2.5.0 + # via flake8 +pygments==2.13.0 + # via + # -r requirements.txt + # ipython +pyhanko==0.13.2 + # via + # -r requirements.txt + # xhtml2pdf +pyhanko-certvalidator==0.19.5 + # via + # -r requirements.txt + # pyhanko + # xhtml2pdf +pyjwt==2.4.0 + # via + # -r requirements.txt + # social-auth-core +pylint==2.15.0 + # via + # -r requirements.dev.in + # -r requirements.txt +pyparsing==3.0.9 + # via + # -r requirements.txt + # httplib2 + # packaging +pypdf3==1.0.6 + # via + # -r requirements.txt + # xhtml2pdf +pyprof2calltree==1.4.5 + # via -r requirements.txt +pysocks==1.7.1 + # via + # -r requirements.txt + # urllib3 +pytest==7.1.3 + # via + # -r requirements.dev.in + # pytest-env pytest-env==0.6.2 -pytest==7.0.1 -python-dateutil==2.6.1 -python-editor==1.0.3 -python-slugify==1.2.5 -python3-openid==3.1.0 -pytz==2017.2 -pyvirtualdisplay==2.2 -pyzmq==16.0.2 -raven[flask]==6.9.0 -remote-pdb==1.3.0 -reportlab==3.5.21 -requests-oauthlib==0.8.0 -requests==2.21.0 -rsa==4.0 -selenium==3.141.0 -sentry-sdk==0.20.3 -six==1.10.0 -social-auth-app-flask-sqlalchemy==1.0.1 + # via -r requirements.dev.in +python-bidi==0.4.2 + # via + # -r requirements.txt + # xhtml2pdf +python-dateutil==2.8.2 + # via + # -r requirements.txt + # pandas +python-jose==3.3.0 + # via + # -r requirements.txt + # social-auth-core +python-slugify==6.1.2 + # via -r requirements.txt +python3-openid==3.2.0 + # via + # -r requirements.txt + # social-auth-core +pytz==2022.2.1 + # via + # -r requirements.txt + # babel + # pandas + # pyhanko +pytz-deprecation-shim==0.1.0.post0 + # via + # -r requirements.txt + # tzlocal +pyvirtualdisplay==3.0 + # via -r requirements.txt +pyyaml==6.0 + # via + # -r requirements.txt + # pyhanko +pyzmq==22.3.0 + # via + # -r requirements.txt + # locust +qrcode==7.3.1 + # via + # -r requirements.txt + # pyhanko +raven[flask]==6.10.0 + # via -r requirements.txt +remote-pdb==2.1.0 + # via -r requirements.txt +reportlab==3.6.11 + # via + # -r requirements.txt + # svglib + # xhtml2pdf +requests==2.28.1 + # via + # -r requirements.txt + # google-api-core + # locust + # mailjet-rest + # pyhanko + # pyhanko-certvalidator + # requests-oauthlib + # social-auth-core +requests-oauthlib==1.3.1 + # via + # -r requirements.txt + # google-auth-oauthlib + # social-auth-core +roundrobin==0.0.4 + # via + # -r requirements.txt + # locust +rsa==4.9 + # via + # -r requirements.txt + # google-auth + # python-jose +selenium==4.4.3 + # via -r requirements.txt +sentry-sdk==1.9.8 + # via -r requirements.txt +six==1.16.0 + # via + # -r requirements.txt + # asttokens + # ecdsa + # flask-cors + # geventhttpclient + # google-auth + # google-auth-httplib2 + # html5lib + # python-bidi + # python-dateutil + # social-auth-app-flask + # social-auth-app-flask-sqlalchemy + # social-auth-storage-sqlalchemy +sniffio==1.3.0 + # via + # -r requirements.txt + # trio social-auth-app-flask==1.0.0 -social-auth-core[openidconnect]==1.4.0 -social-auth-storage-sqlalchemy==1.1.0 + # via + # -r requirements.txt + # social-auth-app-flask-sqlalchemy +social-auth-app-flask-sqlalchemy==1.0.1 + # via -r requirements.txt +social-auth-core[openidconnect]==4.2.0 + # via + # -r requirements.txt + # social-auth-app-flask + # social-auth-app-flask-sqlalchemy + # social-auth-storage-sqlalchemy +sortedcontainers==2.4.0 + # via + # -r requirements.txt + # trio speaklater==1.3 + # via + # -r requirements.txt + # flask-babelex +sqlalchemy==1.3.24 + # via + # -r requirements.txt + # alembic + # labonneboite-common + # social-auth-app-flask-sqlalchemy + # social-auth-storage-sqlalchemy + # sqlalchemy-utils sqlalchemy-stubs==0.4 -sqlalchemy-utils==0.32.13 + # via -r requirements.dev.in +sqlalchemy-utils==0.38.3 + # via -r requirements.txt sqlalchemy2-stubs==0.0.2a27 -sqlalchemy==1.3.3 + # via -r requirements.dev.in +stack-data==0.5.0 + # via + # -r requirements.txt + # ipython +svglib==1.4.1 + # via + # -r requirements.txt + # xhtml2pdf +text-unidecode==1.3 + # via + # -r requirements.txt + # python-slugify +tinycss2==1.1.1 + # via + # -r requirements.txt + # cssselect2 + # svglib toml==0.10.2 -tomli==1.2.3 # via mypy, pytest -traitlets==4.3.2 -typed-ast==1.5.4 # via mypy -types-click==7.1.8 # via types-flask + # via + # -r requirements.txt + # ipdb +tomli==2.0.1 + # via + # -r requirements.txt + # build + # mypy + # pep517 + # pylint + # pytest +tomlkit==0.11.4 + # via + # -r requirements.txt + # pylint +tqdm==4.64.1 + # via + # -r requirements.txt + # pypdf3 +traitlets==5.3.0 + # via + # -r requirements.txt + # ipython + # matplotlib-inline +trio==0.21.0 + # via + # -r requirements.txt + # selenium + # trio-websocket +trio-websocket==0.9.2 + # via + # -r requirements.txt + # selenium +types-click==7.1.8 + # via types-flask types-cryptography==3.3.23 -types-flask==1.1.6 + # via -r requirements.dev.in +types-flask==0.1.2 + # via -r requirements.dev.in types-html5lib==1.1.10 -types-jinja2==2.11.9 # via types-flask -types-markupsafe==1.1.10 + # via -r requirements.dev.in +types-jinja2==2.11.9 + # via types-flask +types-markupsafe==1.1.1 + # via + # -r requirements.dev.in + # types-jinja2 types-mysqlclient==2.1.5 + # via -r requirements.dev.in types-python-slugify==6.1.0 + # via -r requirements.dev.in +types-pytz==2022.2.1.0 + # via pandas-stubs types-requests==2.28.9 + # via -r requirements.dev.in types-selenium==3.141.9 -types-urllib3==1.26.23 # via types-requests -types-werkzeug==1.0.9 # via types-flask -typing-extensions==4.1.1 + # via -r requirements.dev.in +types-urllib3==1.26.23 + # via types-requests +types-werkzeug==1.0.9 + # via types-flask +typing-extensions==4.3.0 + # via + # -r requirements.txt + # locust + # mypy + # sqlalchemy-stubs + # sqlalchemy2-stubs +tzdata==2022.2 + # via + # -r requirements.txt + # pytz-deprecation-shim +tzlocal==4.2 + # via + # -r requirements.txt + # pyhanko unidecode==0.4.21 -uritemplate==3.0.1 -urllib3==1.24.3 -uwsgi==2.0.18 + # via + # -r requirements.txt + # labonneboite-common +uritemplate==4.1.1 + # via + # -r requirements.txt + # google-api-python-client +uritools==4.0.0 + # via + # -r requirements.txt + # pyhanko-certvalidator +urllib3[socks]==1.26.12 + # via + # -r requirements.txt + # elasticsearch + # requests + # selenium + # sentry-sdk +uwsgi==2.0.20 + # via -r requirements.txt uwsgitop==0.11 -validators==0.11.2 -wcwidth==0.1.7 -webassets==0.12.1 + # via -r requirements.txt +validators==0.20.0 + # via -r requirements.txt +wcwidth==0.2.5 + # via + # -r requirements.txt + # prompt-toolkit +webassets==2.0 + # via + # -r requirements.txt + # flask-assets webencodings==0.5.1 -werkzeug==2.0.3 -wrapt==1.10.10 -wtforms==2.1 -xhtml2pdf==0.2.2 + # via + # -r requirements.txt + # cssselect2 + # html5lib + # tinycss2 +werkzeug==2.2.2 + # via + # -r requirements.txt + # flask + # flask-debugtoolbar + # flask-login + # locust +wheel==0.37.1 + # via + # -r requirements.txt + # pip-tools +wrapt==1.14.1 + # via + # -r requirements.txt + # astroid +wsproto==1.2.0 + # via + # -r requirements.txt + # trio-websocket +wtforms==3.0.1 + # via + # -r requirements.txt + # flask-admin + # flask-wtf +xhtml2pdf==0.2.8 + # via -r requirements.txt yapf==0.32.0 + # via -r requirements.dev.in zipp==3.6.0 + # via -r requirements.txt + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/requirements.in b/requirements.in index fe299814f..c4aca8ee0 100644 --- a/requirements.in +++ b/requirements.in @@ -28,9 +28,6 @@ ipython pylint pip-tools -# force importlib version for flake8 compatibility -importlib-metadata<4.3 - # profiling tools used in create_index.py # pycallgraph pyprof2calltree @@ -56,10 +53,10 @@ elasticsearch<2.0.0 Flask-Login social-auth-app-flask-sqlalchemy social-auth-app-flask -social-auth-core[openidconnect] +social-auth-core[openidconnect]==4.2.0 +-e git+https://github.com/python-social-auth/social-storage-sqlalchemy.git@b489c10244a45ae889c554bddc6ab69a7a7e5057#egg=social-auth-storage-sqlalchemy # Unit tests. -nose selenium PyVirtualDisplay Flask-Testing @@ -79,6 +76,11 @@ validators geopy mailjet_rest cryptography +astroid +python-dateutil +email_validator +-e git+https://github.com/zopefoundation/zope.interface.git@24bd6eefaecf2195977a139cdaff2ccfef4e85cd#egg=zope.interface +-e git+https://github.com/zopefoundation/zope.event.git@cdd8d1976ef48d4a59ba49d57d4c956456640c1c#egg=zope.event # Deployment and production uWSGI @@ -86,7 +88,7 @@ uwsgitop raven[flask] # Load testing. -locustio +locust pyzmq remote-pdb diff --git a/requirements.txt b/requirements.txt index 408f6acc4..32d7dc14a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,126 +1,503 @@ # -# This file is autogenerated by pip-compile +# This file is autogenerated by pip-compile with python 3.10 # To update, run: # -# pip-compile --output-file requirements.txt requirements.in +# pip-compile --output-file=requirements.txt requirements.in # -e git+https://github.com/StartupsPoleEmploi/labonneboite-common.git@f0dadcbb79338522169e586a9f4ec0a750920d00#egg=labonneboite-common -alembic==0.9.10 -astroid==1.6.5 # via pylint -babel==2.6.0 # via flask-babelex -backcall==0.1.0 # via ipython -blinker==1.4 # via flask-debugtoolbar, raven -cachetools==4.0.0 # via google-auth -certifi==2017.4.17 # via requests, sentry-sdk -cffi==1.13.1 # via cryptography -chardet==3.0.4 # via requests -click==8.0.4 # via flask, pip-tools -cryptography==2.8 + # via -r requirements.in +-e git+https://github.com/python-social-auth/social-storage-sqlalchemy.git@b489c10244a45ae889c554bddc6ab69a7a7e5057#egg=social-auth-storage-sqlalchemy + # via + # -r requirements.in + # social-auth-app-flask-sqlalchemy +-e git+https://github.com/zopefoundation/zope.event.git@cdd8d1976ef48d4a59ba49d57d4c956456640c1c#egg=zope.event + # via + # -r requirements.in + # gevent +-e git+https://github.com/zopefoundation/zope.interface.git@24bd6eefaecf2195977a139cdaff2ccfef4e85cd#egg=zope.interface + # via + # -r requirements.in + # gevent +alembic==1.8.1 + # via -r requirements.in +arabic-reshaper==2.1.3 + # via xhtml2pdf +asn1crypto==1.5.1 + # via + # oscrypto + # pyhanko + # pyhanko-certvalidator +astroid==2.12.5 + # via + # -r requirements.in + # pylint +asttokens==2.0.8 + # via stack-data +async-generator==1.10 + # via + # trio + # trio-websocket +attrs==22.1.0 + # via + # outcome + # trio +babel==2.10.3 + # via flask-babelex +backcall==0.2.0 + # via ipython +blinker==1.5 + # via + # flask-debugtoolbar + # raven +brotli==1.0.9 + # via geventhttpclient +build==0.8.0 + # via pip-tools +cachetools==5.2.0 + # via google-auth +certifi==2022.6.15 + # via + # geventhttpclient + # requests + # selenium + # sentry-sdk +cffi==1.15.1 + # via cryptography +charset-normalizer==2.1.1 + # via requests +click==8.1.3 + # via + # flask + # pip-tools + # pyhanko +configargparse==1.5.3 + # via locust +cryptography==37.0.4 + # via + # -r requirements.in + # pyhanko + # pyhanko-certvalidator + # social-auth-core cssmin==0.2.0 -dataclasses==0.8 # via werkzeug -decorator==4.3.0 # via ipdb, ipython, traitlets, validators -defusedxml==0.5.0 # via python3-openid, social-auth-core -easyprocess==0.3 # via pyvirtualdisplay + # via -r requirements.in +cssselect2==0.6.0 + # via svglib +decorator==5.1.1 + # via + # ipdb + # ipython + # validators +defusedxml==0.7.1 + # via + # python3-openid + # social-auth-core +dill==0.3.5.1 + # via pylint +dnspython==2.2.1 + # via email-validator +ecdsa==0.18.0 + # via python-jose elasticsearch==1.9.0 -first==2.0.1 # via pip-tools + # via -r requirements.in +email-validator==1.2.1 + # via -r requirements.in +executing==1.0.0 + # via stack-data +flask==2.2.2 + # via + # -r requirements.in + # flask-admin + # flask-assets + # flask-babelex + # flask-basicauth + # flask-cors + # flask-debugtoolbar + # flask-login + # flask-script + # flask-testing + # flask-wtf + # locust + # raven flask-admin==1.6.0 -flask-assets==0.12 + # via -r requirements.in +flask-assets==2.0 + # via -r requirements.in flask-babelex==0.9.4 + # via -r requirements.in +flask-basicauth==0.2.0 + # via locust flask-cors==3.0.10 + # via + # -r requirements.in + # locust flask-debugtoolbar==0.13.1 -flask-login==0.4.1 + # via -r requirements.in +flask-login==0.6.2 + # via + # -r requirements.in + # social-auth-app-flask flask-script==2.0.6 + # via -r requirements.in flask-testing==0.8.1 + # via -r requirements.in flask-wtf==1.0.1 -flask==2.0.3 -future==0.16.0 # via pyjwkest -geographiclib==1.49 # via geopy -geopy==1.19.0 -gevent==1.1.1 # via locustio -google-api-python-client==1.7.11 -google-auth-httplib2==0.0.3 -google-auth-oauthlib==0.4.1 -google-auth==1.11.0 # via google-api-python-client, google-auth-httplib2, google-auth-oauthlib -greenlet==0.4.12 # via gevent -html5lib==1.0.1 -httplib2==0.11.3 # via google-api-python-client, google-auth-httplib2, xhtml2pdf -idna==2.5 # via requests -importlib-metadata==4.2.0 + # via -r requirements.in +future==0.18.2 + # via arabic-reshaper +geographiclib==1.52 + # via geopy +geopy==2.2.0 + # via -r requirements.in +gevent==21.12.0 + # via + # geventhttpclient + # locust +geventhttpclient==2.0.2 + # via locust +google-api-core==2.10.0 + # via google-api-python-client +google-api-python-client==2.58.0 + # via -r requirements.in +google-auth==2.11.0 + # via + # google-api-core + # google-api-python-client + # google-auth-httplib2 + # google-auth-oauthlib +google-auth-httplib2==0.1.0 + # via + # -r requirements.in + # google-api-python-client +google-auth-oauthlib==0.5.2 + # via -r requirements.in +googleapis-common-protos==1.56.4 + # via google-api-core +greenlet==1.1.3 + # via gevent +h11==0.13.0 + # via wsproto +html5lib==1.1 + # via + # -r requirements.in + # xhtml2pdf +httplib2==0.20.4 + # via + # google-api-python-client + # google-auth-httplib2 +idna==3.3 + # via + # email-validator + # requests + # trio ipdb==0.13.9 -ipython-genutils==0.2.0 # via traitlets -ipython==7.16.1 -isort==4.2.15 # via pylint -itsdangerous==2.0.1 # via flask, flask-debugtoolbar, flask-wtf -jedi==0.12.0 # via ipython -jinja2==3.0.3 # via flask, flask-babelex -jsmin==3.0.0 -lazy-object-proxy==1.3.1 # via astroid -line-profiler==2.0 -locustio==0.7.5 -mailjet-rest==1.3.3 -mako==1.0.7 # via alembic -markupsafe==2.0.1 -mccabe==0.6.1 # via pylint -msgpack-python==0.5.6 # via locustio -mysqlclient==1.4.2.post1 -nose==1.3.7 -oauthlib==2.0.2 # via requests-oauthlib, social-auth-core -parameterized==0.7.0 -parso==0.2.1 # via jedi -pexpect==4.6.0 # via ipython -pickleshare==0.7.4 # via ipython -pillow==6.0.0 # via reportlab, xhtml2pdf -pip-tools==2.0.2 -prompt-toolkit==3.0.21 # via ipython -ptyprocess==0.6.0 # via pexpect -pyasn1-modules==0.2.8 # via google-auth -pyasn1==0.4.8 # via pyasn1-modules, rsa -pycparser==2.19 # via cffi -pycryptodomex==3.6.3 # via pyjwkest -pygments==2.2.0 # via ipython -pyjwkest==1.4.0 # via social-auth-core -pyjwt==1.5.2 # via social-auth-core -pylint==1.9.2 -pypdf2==1.26.0 # via xhtml2pdf -pyprof2calltree==1.4.3 -python-dateutil==2.6.1 # via alembic -python-editor==1.0.3 # via alembic -python-slugify==1.2.5 -python3-openid==3.1.0 # via social-auth-core -pytz==2017.2 # via babel -pyvirtualdisplay==2.2 -pyzmq==16.0.2 -raven[flask]==6.9.0 -remote-pdb==1.3.0 -reportlab==3.5.21 # via xhtml2pdf -requests-oauthlib==0.8.0 # via google-auth-oauthlib, social-auth-core -requests==2.21.0 -rsa==4.0 # via google-auth -selenium==3.141.0 -sentry-sdk==0.20.3 -six==1.10.0 # via astroid, cryptography, flask-cors, google-api-python-client, google-auth, html5lib, pip-tools, pyjwkest, pylint, python-dateutil, social-auth-app-flask, social-auth-app-flask-sqlalchemy, social-auth-core, social-auth-storage-sqlalchemy, sqlalchemy-utils, traitlets, validators, xhtml2pdf -social-auth-app-flask-sqlalchemy==1.0.1 + # via -r requirements.in +ipython==8.4.0 + # via + # -r requirements.in + # ipdb +isort==5.10.1 + # via pylint +itsdangerous==2.1.2 + # via + # flask + # flask-debugtoolbar + # flask-wtf +jedi==0.18.1 + # via ipython +jinja2==3.1.2 + # via + # flask + # flask-babelex +jsmin==3.0.1 + # via -r requirements.in +lazy-object-proxy==1.7.1 + # via astroid +line-profiler==3.5.1 + # via -r requirements.in +locust==2.11.1 + # via -r requirements.in +lxml==4.9.1 + # via svglib +mailjet-rest==1.3.4 + # via -r requirements.in +mako==1.2.2 + # via alembic +markupsafe==2.1.1 + # via + # -r requirements.in + # jinja2 + # mako + # werkzeug + # wtforms +matplotlib-inline==0.1.6 + # via ipython +mccabe==0.7.0 + # via pylint +msgpack==1.0.4 + # via locust +mysqlclient==2.1.1 + # via -r requirements.in +numpy==1.23.2 + # via pandas +oauthlib==3.2.0 + # via + # requests-oauthlib + # social-auth-core +oscrypto==1.3.0 + # via pyhanko-certvalidator +outcome==1.2.0 + # via trio +packaging==21.3 + # via build +pandas==1.4.4 + # via -r requirements.in +parameterized==0.8.1 + # via -r requirements.in +parso==0.8.3 + # via jedi +pep517==0.13.0 + # via build +pexpect==4.8.0 + # via ipython +pickleshare==0.7.5 + # via ipython +pillow==9.2.0 + # via + # reportlab + # xhtml2pdf +pip-tools==6.8.0 + # via -r requirements.in +platformdirs==2.5.2 + # via pylint +prompt-toolkit==3.0.31 + # via ipython +protobuf==4.21.5 + # via + # google-api-core + # googleapis-common-protos +psutil==5.9.2 + # via locust +ptyprocess==0.7.0 + # via pexpect +pure-eval==0.2.2 + # via stack-data +pyasn1==0.4.8 + # via + # pyasn1-modules + # python-jose + # rsa +pyasn1-modules==0.2.8 + # via google-auth +pycparser==2.21 + # via cffi +pygments==2.13.0 + # via ipython +pyhanko==0.13.2 + # via xhtml2pdf +pyhanko-certvalidator==0.19.5 + # via + # pyhanko + # xhtml2pdf +pyjwt==2.4.0 + # via social-auth-core +pylint==2.15.0 + # via -r requirements.in +pyparsing==3.0.9 + # via + # httplib2 + # packaging +pypdf3==1.0.6 + # via xhtml2pdf +pyprof2calltree==1.4.5 + # via -r requirements.in +pysocks==1.7.1 + # via urllib3 +python-bidi==0.4.2 + # via xhtml2pdf +python-dateutil==2.8.2 + # via + # -r requirements.in + # pandas +python-jose==3.3.0 + # via social-auth-core +python-slugify==6.1.2 + # via -r requirements.in +python3-openid==3.2.0 + # via social-auth-core +pytz==2022.2.1 + # via + # babel + # pandas + # pyhanko +pytz-deprecation-shim==0.1.0.post0 + # via tzlocal +pyvirtualdisplay==3.0 + # via -r requirements.in +pyyaml==6.0 + # via pyhanko +pyzmq==22.3.0 + # via + # -r requirements.in + # locust +qrcode==7.3.1 + # via pyhanko +raven[flask]==6.10.0 + # via -r requirements.in +remote-pdb==2.1.0 + # via -r requirements.in +reportlab==3.6.11 + # via + # svglib + # xhtml2pdf +requests==2.28.1 + # via + # -r requirements.in + # google-api-core + # locust + # mailjet-rest + # pyhanko + # pyhanko-certvalidator + # requests-oauthlib + # social-auth-core +requests-oauthlib==1.3.1 + # via + # google-auth-oauthlib + # social-auth-core +roundrobin==0.0.4 + # via locust +rsa==4.9 + # via + # google-auth + # python-jose +selenium==4.4.3 + # via -r requirements.in +sentry-sdk==1.9.8 + # via -r requirements.in +six==1.16.0 + # via + # asttokens + # ecdsa + # flask-cors + # geventhttpclient + # google-auth + # google-auth-httplib2 + # html5lib + # python-bidi + # python-dateutil + # social-auth-app-flask + # social-auth-app-flask-sqlalchemy + # social-auth-storage-sqlalchemy +sniffio==1.3.0 + # via trio social-auth-app-flask==1.0.0 -social-auth-core[openidconnect]==1.4.0 -social-auth-storage-sqlalchemy==1.1.0 # via social-auth-app-flask-sqlalchemy + # via + # -r requirements.in + # social-auth-app-flask-sqlalchemy +social-auth-app-flask-sqlalchemy==1.0.1 + # via -r requirements.in +social-auth-core[openidconnect]==4.2.0 + # via + # -r requirements.in + # social-auth-app-flask + # social-auth-app-flask-sqlalchemy + # social-auth-storage-sqlalchemy +sortedcontainers==2.4.0 + # via trio speaklater==1.3 -sqlalchemy-utils==0.32.13 -sqlalchemy==1.3.3 -toml==0.10.2 # via ipdb -traitlets==4.3.2 # via ipython -typing-extensions==4.1.1 # via importlib-metadata -unidecode==0.4.21 # via python-slugify -uritemplate==3.0.1 # via google-api-python-client -urllib3==1.24.3 # via elasticsearch, requests, selenium, sentry-sdk -uwsgi==2.0.18 + # via + # -r requirements.in + # flask-babelex +sqlalchemy==1.3.24 + # via + # -r requirements.in + # alembic + # labonneboite-common + # social-auth-app-flask-sqlalchemy + # social-auth-storage-sqlalchemy + # sqlalchemy-utils +sqlalchemy-utils==0.38.3 + # via -r requirements.in +stack-data==0.5.0 + # via ipython +svglib==1.4.1 + # via xhtml2pdf +text-unidecode==1.3 + # via python-slugify +tinycss2==1.1.1 + # via + # cssselect2 + # svglib +toml==0.10.2 + # via ipdb +tomli==2.0.1 + # via + # build + # pep517 + # pylint +tomlkit==0.11.4 + # via pylint +tqdm==4.64.1 + # via pypdf3 +traitlets==5.3.0 + # via + # ipython + # matplotlib-inline +trio==0.21.0 + # via + # selenium + # trio-websocket +trio-websocket==0.9.2 + # via selenium +typing-extensions==4.3.0 + # via locust +tzdata==2022.2 + # via pytz-deprecation-shim +tzlocal==4.2 + # via pyhanko +unidecode==0.4.21 + # via labonneboite-common +uritemplate==4.1.1 + # via google-api-python-client +uritools==4.0.0 + # via pyhanko-certvalidator +urllib3[socks]==1.26.12 + # via + # elasticsearch + # requests + # selenium + # sentry-sdk +uwsgi==2.0.20 + # via -r requirements.in uwsgitop==0.11 -validators==0.11.2 -wcwidth==0.1.7 # via prompt-toolkit -webassets==0.12.1 # via flask-assets -webencodings==0.5.1 # via html5lib -werkzeug==2.0.3 # via flask, flask-debugtoolbar -wrapt==1.10.10 # via astroid -wtforms==2.1 -xhtml2pdf==0.2.2 + # via -r requirements.in +validators==0.20.0 + # via -r requirements.in +wcwidth==0.2.5 + # via prompt-toolkit +webassets==2.0 + # via flask-assets +webencodings==0.5.1 + # via + # cssselect2 + # html5lib + # tinycss2 +werkzeug==2.2.2 + # via + # flask + # flask-debugtoolbar + # flask-login + # locust +wheel==0.37.1 + # via pip-tools +wrapt==1.14.1 + # via astroid +wsproto==1.2.0 + # via trio-websocket +wtforms==3.0.1 + # via + # -r requirements.in + # flask-admin + # flask-wtf +xhtml2pdf==0.2.8 + # via -r requirements.in zipp==3.6.0 -pandas \ No newline at end of file