diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..905a527 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,34 @@ +# https://editorconfig.org/ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.py] +indent_size = 4 +max_line_length = 88 + +[Makefile] +indent_style = tab + +[*.md] +trim_trailing_whitespace = false + +[Pipfile.lock] +indent_size = 2 + +[{Pipfile,*.toml}] +indent_size = 4 + +[*.{js,jsx,ts,tsx,vue}] +indent_style = space +indent_size = 2 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 80 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..194dbb4 --- /dev/null +++ b/.env.example @@ -0,0 +1,32 @@ +EURYDICE_VERSION="latest" + +LIDI_VERSION="latest" +LIDIR_HOST="127.0.0.1" + +DJANGO_SECRET_KEY="fgjd" + +TRANSFERABLE_HISTORY_DURATION="15min" +TRANSFERABLE_HISTORY_SEND_EVERY="1min" +TRANSFERABLE_RANGE_SIZE="150MB" + +UI_BADGE_CONTENT="Local -> Local" +UI_BADGE_COLOR="brown" + +MINIO_ENABLED="true" +MINIO_DATA_DIR="/tmp/eurydice/minio-data" +MINIO_CONF_DIR="/tmp/eurydice/minio-conf" +MINIO_ACCESS_KEY="minio" +MINIO_SECRET_KEY="gjhfdfdsh" +TRANSFERABLE_STORAGE_DIR="/tmp/eurydice/storage-data" + +DB_PASSWORD="dbpass" +DB_DATA_DIR="/tmp/eurydice/db-data" +DB_LOGS_DIR="/tmp/eurydice/db-logs" + +FILEBEAT_LOGS_DIR="/tmp/eurydice/filebeat-logs" +FILEBEAT_DATA_DIR="/tmp/eurydice/filebeat-data" +FILEBEAT_CONFIG_PATH="./filebeat/filebeat.null.yml" + +LOG_TO_FILE=true +PYTHON_LOGS_DIR="/tmp/eurydice/python-logs/" +ELASTICSEARCH_CERT_PATH="/etc/ssl/certs/ca-certificates.crt" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fb9df8c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,85 @@ +name: Checks then build and push + +on: + push: + branches: + - master + +jobs: + backend-checks: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.10' # Specify the Python version you need + + - name: Install pipenv + run: | + python -m pip install --upgrade pip + pip install pipenv + + - name: Install dependencies + run: | + cd backend + pipenv install --dev + + - name: Run checks + run: | + cd backend + pipenv run make checks + + frontend-checks: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: '18' + + - name: Install dependencies + run: | + cd frontend + npm install + + - name: Run checks + run: | + cd frontend + make lint + + build_and_push: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + + steps: + - uses: actions/checkout@v4 + + - name: DockerHub Login + uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Docker build the backend docker image + run: | + docker build --target prod --build-arg EURYDICE_VERSION=${GITHUB_REF#refs/tags/} --file ./backend/docker/Dockerfile --tag anssi/eurydice-backend:${GITHUB_REF#refs/tags/} backend + + - name: Docker build the frontend docker image + run: | + docker build --target prod --build-arg EURYDICE_VERSION=${GITHUB_REF#refs/tags/} --file ./frontend/docker/Dockerfile --tag anssi/eurydice-frontend:${GITHUB_REF#refs/tags/} frontend + + - name: Docker push backend docker image + run: | + docker push anssi/eurydice-backend:${GITHUB_REF#refs/tags/} + + - name: Docker push frontend docker image + run: | + docker push anssi/eurydice-frontend:${GITHUB_REF#refs/tags/} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..94ae0c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,272 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +*junit-report.xml +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ +.report/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pycharm +.idea + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Crash log files +crash.log + +staticfiles/ + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test +.env.production + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# vim +.*.swp + +data/ diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..e0a9732 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,42 @@ +# Project File Structure + +``` +eurydice +├── backend/ +| ├── docker/ # files related to building the docker image +| ├── eurydice/ # backend's source code +| | ├── common/ # common code for origin and destination +| | ├── destination/ # destination services +| │ └── origin/ # origin services +| │ ├── api/ # origin API +| │ ├── backoffice/ # Django admin interface of the origin +| │ ├── cleaning/ # additional clean-up services (dbtrimmer, s3remover) +| | ├── config/ # configuration of the origin Django project +| | ├── core/ # code common to the "API" and "sender" services of the origin +| │ └── sender/ # service sending data packets to lidis +| └── tests/ # tests for the backend +| ├── common/ # tests of the common code +| ├── destination/ # tests of destination services +| └── origin/ # tests of origin services +| ├── integration/ +| └── unit/ +├── docs/ # additional misc. documentation +├── filebeat/ # sample configuration files for filebeat container +├── frontend/ +| ├── docker/ # files related to building the docker image +| ├── public/ # frontend's static files +| ├── src/ # frontend's source code +| | ├── common/ # common code for origin and destination frontends +| | | ├── api/ # source code for making requests to the API +| | | ├── components/ +| | | ├── layouts/ +| | | ├── plugins/ # VueJS plugins +| | | ├── utils/ # miscellaneous common utilities +| | | └── views/ +| | ├── destination/ # destination frontend +| │ └── origin/ # origin frontend +| └── tests/ # tests for the frontend +├── pgadmin/ # pgadmin container configuration +├── compose.*.yml # sample docker compose configurations +└── README.md +``` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..290c969 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +# ------------------------------------------------------------------------------ +# Static Analysis +# ------------------------------------------------------------------------------ + +.PHONY: hadolint +hadolint: ## Lint the Dockerfiles. + docker run --rm -i hadolint/hadolint:2.8.0-alpine < backend/docker/Dockerfile + docker run --rm -i hadolint/hadolint:2.8.0-alpine < frontend/docker/Dockerfile + docker run --rm -i hadolint/hadolint:2.8.0-alpine < pgadmin/Dockerfile diff --git a/README.md b/README.md new file mode 100644 index 0000000..8236fcb --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +
+ Eurydice icon +
+ +

Eurydice

+ +
+Eurydice (Emetteur Unidirectionnel Redondant de Yottabit pour le Dialogue Intergiciel avec Correction d'Erreur) provides an interface to transfer files through a physical diode using the [Lidi](https://github.com/ANSSI-FR/lidi/) utility. +
+ +## 📁 Project structure + +See [ARCHITECTURE.md](ARCHITECTURE.md). + +## 📦 Versioning + +This project adheres to the [Semantic Versioning specification](https://semver.org/). +Versions are represented as [git tags](https://github.com/ANSSI-FR/eurydice/tags). +You can view the release history with the accompanying changelog [here](https://github.com/ANSSI-FR/eurydice/releases). + +## 🔨 Prerequisites + +- [`docker>=19.03.0`](https://docs.docker.com/engine/install/) + - support for the compose specification [was added in `19.03.0`](https://docs.docker.com/compose/compose-file/compose-versioning/#compatibility-matrix) + +## 🐳 Docker images + +The compiled docker image are publicly available on the docker hub. + +Dockerfiles are provided for each component. + +The following images are available: + +- `anssi/eurydice-backend` + - docker image for the backend service (API, sender, database trimmer) +- `anssi/eurydice-frontend` + - docker image for the server responsible for serving the frontend + +The following tags are available for the aforementioned docker images: + +- `0.x.x` docker image built for a specific tagged release + +## 🚧 Development + +See [docs/developers.md](docs/developers.md). + +## 🚀 Deployment in production & administration + +See [docs/administrators.md](docs/administrators.md). + +## ▶️ Usage + +### 👩‍💻 HTTP API + +See documentation directories: + +- [Common API documentation](eurydice/common/api/docs/static/). +- [Origin API documentation and code snippets](eurydice/origin/api/docs/static/). +- [Destination API documentation and code snippets](eurydice/destination/api/docs/static/). + +## 🙏 Credits + +- Logo by [Freepik](https://www.freepik.com) diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..bda359e --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,19 @@ +* +!docker/entrypoint.sh +!docker/healthcheck.py +!eurydice +!tests +!Makefile +!manage.py +!Pipfile* +!pyproject.toml +!setup.cfg +!settings.py + +# ignore static files +**/staticfiles/ + +# Byte-compiled / optimized / DLL files +**/__pycache__/ +**/*.py[cod] +**/*$py.class diff --git a/backend/.safety-policy.yml b/backend/.safety-policy.yml new file mode 100644 index 0000000..422a4b5 --- /dev/null +++ b/backend/.safety-policy.yml @@ -0,0 +1,10 @@ +# Safety Security and License Configuration file +security: + ignore-vulnerabilities: + 51457: + reason: no fix available yet + expires: '2024-03-01' + 62044: + reason: we don't install packages from mercurial + 70612: + reason: no fix available yet diff --git a/backend/Makefile b/backend/Makefile new file mode 100644 index 0000000..cffbed0 --- /dev/null +++ b/backend/Makefile @@ -0,0 +1,181 @@ +SRC_DIR := eurydice +PACKAGES := common origin destination +TESTS_DIR := tests +CODE_DIRS := $(SRC_DIR) $(TESTS_DIR) +DUMMY_MINIO_ENV_VARIABLES := MINIO_ENDPOINT="" MINIO_ACCESS_KEY="" MINIO_SECRET_KEY="" TRANSFERABLE_STORAGE_DIR="/tmp/" +DUMMY_DB_ENV_VARIABLES := DB_NAME="" DB_USER="" DB_PASSWORD="" DB_HOST="" DB_PORT="" +DUMMY_LIDIS_ENV_VARIABLES := LIDIS_HOST="127.0.0.1" LIDIS_PORT="1" + +# ------------------------------------------------------------------------------ +# Dependency management +# ------------------------------------------------------------------------------ + +.PHONY: install +install: ## Install dependencies on production environment. + PIPENV_VENV_IN_PROJECT=true pipenv install --deploy --ignore-pipfile + +.PHONY: install-dev +install-dev: ## Install development dependencies. + pipenv install --dev --deploy + +.PHONY: install-dev-docker +install-dev-docker: ## Install development dependencies. + pipenv install --dev --deploy --system + +# ------------------------------------------------------------------------------ +# Application management +# ------------------------------------------------------------------------------ + +.PHONY: run-dev +run-dev: ## Run the eurydice development server. + python manage.py runserver 0.0.0.0:8080 + +.PHONY: run-origin-api +run-origin-api: ## Run the eurydice origin API production server. + gunicorn -c eurydice/common/config/gunicorn.conf.py $$GUNICORN_CONFIGURATION eurydice.origin.config.wsgi + +.PHONY: run-destination-api +run-destination-api: ## Run the eurydice destination API production server. + gunicorn -c eurydice/common/config/gunicorn.conf.py $$GUNICORN_CONFIGURATION eurydice.destination.config.wsgi + +.PHONY: run-sender +run-sender: ## Run the eurydice sender. + python -m eurydice.origin.sender + +.PHONY: run-receiver +run-receiver: ## Run the eurydice receiver. + python -m eurydice.destination.receiver + +.PHONY: run-destination-s3remover +run-destination-s3remover: ## Run the eurydice destination s3remover cleaning tool. + python -m eurydice.destination.cleaning.s3remover + +.PHONY: run-destination-dbtrimmer +run-destination-dbtrimmer: ## Run the eurydice destination dbtrimmer cleaning tool. + python -m eurydice.destination.cleaning.dbtrimmer + +.PHONY: run-origin-dbtrimmer +run-origin-dbtrimmer: ## Run the eurydice origin dbtrimmer cleaning tool. + python -m eurydice.origin.cleaning.dbtrimmer + +.PHONY: superuser +superuser: ## Create Django super user. + DJANGO_SUPERUSER_USERNAME=admin DJANGO_SUPERUSER_PASSWORD=admin DJANGO_SUPERUSER_EMAIL=admin@example.com python manage.py createsuperuser --noinput + +.PHONY: user +user: ## Create Django user with an associated token. + echo "import django; from rest_framework.authtoken.models import Token; user = django.contrib.auth.get_user_model().objects.create_user('user', 'user@example.com', 'user'); print(Token.objects.create(user=user))" | python manage.py shell + +.PHONY: migrations +migrations: ## Create new migrations based on the changes made to the models. + python manage.py makemigrations + +.PHONY: migrations-check +migrations-check: ## Check that models are not changed without the appropriate migrations. + USER_ASSOCIATION_TOKEN_SECRET_KEY="" DJANGO_ENV=TEST EURYDICE_API=origin python manage.py makemigrations --check + USER_ASSOCIATION_TOKEN_SECRET_KEY="" DJANGO_ENV=TEST EURYDICE_API=destination python manage.py makemigrations --check + +.PHONY: migrate +migrate: ## Apply migrations. + python manage.py migrate + +.PHONY: collectstatic +collectstatic: ## Collect static files. + USER_ASSOCIATION_TOKEN_SECRET_KEY="" MINIO_ENABLED=false \ + $(DUMMY_MINIO_ENV_VARIABLES) $(DUMMY_DB_ENV_VARIABLES) $(DUMMY_LIDIS_ENV_VARIABLES) \ + python manage.py collectstatic --no-input --clear --ignore rest_framework + +# ------------------------------------------------------------------------------ +# Static Analysis & Tests +# ------------------------------------------------------------------------------ + +# Overwrite files in place to ensure file flags and mode are unchanged when isort is run +# inside a Docker container. +.PHONY: isort +isort: ## Format imports. + isort --overwrite-in-place $(CODE_DIRS) + +.PHONY: black +black: ## Format code. + black $(CODE_DIRS) + +.PHONY: format +format: ## Format code and imports. + $(MAKE) isort black + +.PHONY: black-check +black-check: ## Check that the code is properly formatted. + black --check $(CODE_DIRS) + +.PHONY: isort-check +isort-check: ## Check that the imports are properly sorted. + isort --check-only $(CODE_DIRS) + +.PHONY: flake8 +flake8: ## Check PEP8, PyFlakes and circular complexity of the code. + flakeheaven lint $(CODE_DIRS) + +.PHONY: mypy +mypy: ## Run MyPy static type check across the code. + export EURYDICE_MYPY_ERROR=0;\ + for dir in $(PACKAGES); do \ + DS=eurydice.$$dir.config.settings.base \ + USER_ASSOCIATION_TOKEN_SECRET_KEY="" \ + DJANGO_ENV=TEST \ + MINIO_ENABLED=false \ + $(DUMMY_MINIO_ENV_VARIABLES) \ + $(DUMMY_DB_ENV_VARIABLES) \ + $(DUMMY_LIDIS_ENV_VARIABLES) \ + mypy --check eurydice/$$dir \ + || EURYDICE_MYPY_ERROR=1; \ + done;\ + if [ "$$EURYDICE_MYPY_ERROR" = '1' ] ; then exit 1; fi + +.PHONY: pytype +pytype: ## Run Pytype static type check across the code. + pytype $(CODE_DIRS) --jobs $$(nproc) + +.PHONY: bandit +bandit: ## Run a security oriented static analysis of the code. + bandit -r --ini setup.cfg + +.PHONY: safety +safety: ## Check installed dependencies for known security vulnerabilities. + safety \ + --disable-telemetry \ + check \ + --full-report + +.PHONY: checks +checks: ## Runs all the static analysis tools (Flake8, MyPy, Bandit and Safety) at once. + $(MAKE) flake8 mypy pytype bandit black-check isort-check safety + +.PHONY: tests +tests: ## Run tests, iterate over test modules to configure DJANGO_SETTINGS_MODULE, to fail correctly, this should be one command. + coverage erase + export EURYDICE_TESTS_ERROR=0;\ + for dir in $(PACKAGES); do \ + USER_ASSOCIATION_TOKEN_SECRET_KEY="" \ + DJANGO_ENV=TEST pytest $(TESTS_DIR)/$$dir \ + --ds=eurydice.$$dir.config.settings.test \ + --cov-append \ + --cov-report= \ + --cov=$(SRC_DIR) \ + --junitxml=.report/$$dir-junit.xml \ + || EURYDICE_TESTS_ERROR=1 ;\ + done; \ + coverage xml; \ + coverage report -m; \ + if [ "$$EURYDICE_TESTS_ERROR" = '1' ] ; then exit 1; fi + + +# ------------------------------------------------------------------------------ +# Help +# ------------------------------------------------------------------------------ + +.PHONY: help +help: ## Show this help. + @echo "Usage:" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.DEFAULT_GOAL := help diff --git a/backend/Pipfile b/backend/Pipfile new file mode 100644 index 0000000..25d22c0 --- /dev/null +++ b/backend/Pipfile @@ -0,0 +1,59 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "mirror" + +[packages] +gunicorn = "==23.*" +djangorestframework = ">=3.15.2" +django-environ = ">=0.11.2" +drf-spectacular = ">=0.27.1" +psycopg2-binary = "==2.9.9" +django = "==4.*" +python-dateutil = "==2.9.0.post0" +humanfriendly = "==10.0" +whitenoise = "==6.7.0" +msgpack = "==1.1.0" +pydantic = "==1.*" +minio = "==7.2.3" +django-filter = ">=24.*" +setuptools = ">=70.*" + +[dev-packages] +pytest = ">=8.*" +pytest-django = ">=4.*" # useful to configure settings in tests even if not explicitly called +factory-boy = "==3.2.1" +hypothesis = "==6.39.4" +coverage = "==6.*" +pytest-cov = ">=4.*" +pytype = "==2024.01.05" +ipython = "==8.10.0" +ipdb = "==0.13.9" +isort = "==5.10.1" +black = "==24.3.0" +flake8 = "==4.0.1" +flake8-bugbear = "==22.3.20" +flake8-comprehensions = "==3.8.0" +flake8-eradicate = "==1.2.0" +flake8-simplify = "==0.18.1" +flake8-variables-names = "==0.0.5" +pep8-naming = "==0.12.1" +flake8-use-fstring = "==1.3" +flake8-annotations = "==2.7.0" +flake8-pytest = "==1.3" +flake8-pytest-style = "==1.6.0" +dlint = "==0.12.0" +flake8-docstrings = "==1.6.0" +flakeheaven = "==0.11.1" +mypy = "==1.11.2" +mypy-extensions = ">=0.4.4" +bandit = "==1.7.7" +Faker = "==13.3.2" +django-stubs = "==1.9.0" +djangorestframework-stubs = "==1.4.0" +radon = "==5.1.0" +freezegun = "==1.2.1" +safety = "==2.*" + +[requires] +python_version = "3.10" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock new file mode 100644 index 0000000..8e9a692 --- /dev/null +++ b/backend/Pipfile.lock @@ -0,0 +1,2124 @@ +{ + "_meta": { + "hash": { + "sha256": "20dca5a6f10ac6739ac1c4e4614025b3529f0b959c11c9260ab8252b07380edd" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.10" + }, + "sources": [ + { + "name": "mirror", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "argon2-cffi": { + "hashes": [ + "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08", + "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea" + ], + "markers": "python_version >= '3.7'", + "version": "==23.1.0" + }, + "argon2-cffi-bindings": { + "hashes": [ + "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670", + "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f", + "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583", + "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194", + "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", + "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a", + "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", + "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5", + "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", + "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7", + "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", + "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", + "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", + "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", + "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", + "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", + "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d", + "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", + "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb", + "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", + "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351" + ], + "markers": "python_version >= '3.6'", + "version": "==21.2.0" + }, + "asgiref": { + "hashes": [ + "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", + "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590" + ], + "markers": "python_version >= '3.8'", + "version": "==3.8.1" + }, + "attrs": { + "hashes": [ + "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", + "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2" + ], + "markers": "python_version >= '3.7'", + "version": "==24.2.0" + }, + "certifi": { + "hashes": [ + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" + ], + "markers": "python_version >= '3.6'", + "version": "==2024.8.30" + }, + "cffi": { + "hashes": [ + "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", + "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", + "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", + "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", + "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", + "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", + "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", + "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", + "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", + "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", + "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", + "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", + "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", + "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", + "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", + "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", + "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", + "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", + "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", + "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", + "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", + "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", + "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", + "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", + "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", + "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", + "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", + "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", + "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", + "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", + "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", + "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", + "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", + "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", + "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", + "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", + "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", + "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", + "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", + "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", + "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", + "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", + "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", + "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", + "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", + "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", + "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", + "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", + "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", + "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", + "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", + "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", + "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", + "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", + "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", + "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", + "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", + "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", + "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", + "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", + "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", + "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", + "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", + "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", + "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", + "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", + "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" + ], + "markers": "python_version >= '3.8'", + "version": "==1.17.1" + }, + "django": { + "hashes": [ + "sha256:1ddc333a16fc139fd253035a1606bb24261951bbc3a6ca256717fa06cc41a898", + "sha256:6f1616c2786c408ce86ab7e10f792b8f15742f7b7b7460243929cb371e7f1dad" + ], + "index": "mirror", + "version": "==4.2.16" + }, + "django-environ": { + "hashes": [ + "sha256:0ff95ab4344bfeff693836aa978e6840abef2e2f1145adff7735892711590c05", + "sha256:f32a87aa0899894c27d4e1776fa6b477e8164ed7f6b3e410a62a6d72caaf64be" + ], + "index": "mirror", + "version": "==0.11.2" + }, + "django-filter": { + "hashes": [ + "sha256:c4852822928ce17fb699bcfccd644b3574f1a2d80aeb2b4ff4f16b02dd49dc64", + "sha256:d8ccaf6732afd21ca0542f6733b11591030fa98669f8d15599b358e24a2cd9c3" + ], + "index": "mirror", + "version": "==24.3" + }, + "djangorestframework": { + "hashes": [ + "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20", + "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad" + ], + "index": "mirror", + "version": "==3.15.2" + }, + "drf-spectacular": { + "hashes": [ + "sha256:a199492f2163c4101055075ebdbb037d59c6e0030692fc83a1a8c0fc65929981", + "sha256:b1c04bf8b2fbbeaf6f59414b4ea448c8787aba4d32f76055c3b13335cf7ec37b" + ], + "index": "mirror", + "version": "==0.27.2" + }, + "gunicorn": { + "hashes": [ + "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", + "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" + ], + "index": "mirror", + "version": "==23.0.0" + }, + "humanfriendly": { + "hashes": [ + "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", + "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc" + ], + "index": "mirror", + "version": "==10.0" + }, + "inflection": { + "hashes": [ + "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", + "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2" + ], + "markers": "python_version >= '3.5'", + "version": "==0.5.1" + }, + "jsonschema": { + "hashes": [ + "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", + "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566" + ], + "markers": "python_version >= '3.8'", + "version": "==4.23.0" + }, + "jsonschema-specifications": { + "hashes": [ + "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", + "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf" + ], + "markers": "python_version >= '3.9'", + "version": "==2024.10.1" + }, + "minio": { + "hashes": [ + "sha256:4971dfb1a71eeefd38e1ce2dc7edc4e6eb0f07f1c1d6d70c15457e3280cfc4b9", + "sha256:e6b5ce0a9b4368da50118c3f0c4df5dbf33885d44d77fce6c0aa1c485e6af7a1" + ], + "index": "mirror", + "version": "==7.2.3" + }, + "msgpack": { + "hashes": [ + "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b", + "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf", + "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca", + "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330", + "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f", + "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f", + "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39", + "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247", + "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b", + "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c", + "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7", + "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044", + "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6", + "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b", + "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0", + "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2", + "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468", + "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7", + "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734", + "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434", + "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325", + "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1", + "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846", + "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88", + "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420", + "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e", + "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2", + "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59", + "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb", + "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68", + "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915", + "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f", + "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701", + "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b", + "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d", + "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa", + "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d", + "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd", + "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc", + "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48", + "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb", + "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74", + "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b", + "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346", + "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e", + "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6", + "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5", + "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f", + "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5", + "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b", + "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c", + "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f", + "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec", + "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8", + "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5", + "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d", + "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e", + "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", + "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870", + "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f", + "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96", + "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c", + "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd", + "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788" + ], + "index": "mirror", + "version": "==1.1.0" + }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, + "psycopg2-binary": { + "hashes": [ + "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9", + "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77", + "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e", + "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84", + "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3", + "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2", + "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67", + "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876", + "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152", + "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f", + "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a", + "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6", + "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503", + "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f", + "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493", + "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996", + "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f", + "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e", + "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59", + "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94", + "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7", + "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682", + "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420", + "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae", + "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291", + "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe", + "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980", + "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93", + "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692", + "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119", + "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716", + "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472", + "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b", + "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2", + "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc", + "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c", + "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5", + "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab", + "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984", + "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9", + "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf", + "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0", + "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f", + "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212", + "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb", + "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be", + "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90", + "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041", + "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7", + "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860", + "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d", + "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245", + "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27", + "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417", + "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359", + "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202", + "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0", + "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7", + "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba", + "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1", + "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd", + "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07", + "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98", + "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55", + "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d", + "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972", + "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f", + "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e", + "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26", + "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957", + "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53", + "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52" + ], + "index": "mirror", + "version": "==2.9.9" + }, + "pycparser": { + "hashes": [ + "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc" + ], + "markers": "python_version >= '3.8'", + "version": "==2.22" + }, + "pycryptodome": { + "hashes": [ + "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8", + "sha256:0fa0a05a6a697ccbf2a12cec3d6d2650b50881899b845fac6e87416f8cb7e87d", + "sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0", + "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93", + "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4", + "sha256:26412b21df30b2861424a6c6d5b1d8ca8107612a4cfa4d0183e71c5d200fb34a", + "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764", + "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca", + "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e", + "sha256:3ba4cc304eac4d4d458f508d4955a88ba25026890e8abff9b60404f76a62c55e", + "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd", + "sha256:590ef0898a4b0a15485b05210b4a1c9de8806d3ad3d47f74ab1dc07c67a6827f", + "sha256:5dfafca172933506773482b0e18f0cd766fd3920bd03ec85a283df90d8a17bc6", + "sha256:6cce52e196a5f1d6797ff7946cdff2038d3b5f0aba4a43cb6bf46b575fd1b5bb", + "sha256:7cb087b8612c8a1a14cf37dd754685be9a8d9869bed2ffaaceb04850a8aeef7e", + "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1", + "sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6", + "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a", + "sha256:8acd7d34af70ee63f9a849f957558e49a98f8f1634f86a59d2be62bb8e93f71c", + "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2", + "sha256:a1752eca64c60852f38bb29e2c86fca30d7672c024128ef5d70cc15868fa10f4", + "sha256:a3804675283f4764a02db05f5191eb8fec2bb6ca34d466167fc78a5f05bbe6b3", + "sha256:a4e74c522d630766b03a836c15bff77cb657c5fdf098abf8b1ada2aebc7d0819", + "sha256:a915597ffccabe902e7090e199a7bf7a381c5506a747d5e9d27ba55197a2c568", + "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53", + "sha256:cc2269ab4bce40b027b49663d61d816903a4bd90ad88cb99ed561aadb3888dd3", + "sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8", + "sha256:dad9bf36eda068e89059d1f07408e397856be9511d7113ea4b586642a429a4fd", + "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b", + "sha256:f35e442630bc4bc2e1878482d6f59ea22e280d7121d7adeaedba58c23ab6386b", + "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297", + "sha256:ff99f952db3db2fbe98a0b355175f93ec334ba3d01bbde25ad3a5a33abc02b58" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==3.21.0" + }, + "pydantic": { + "hashes": [ + "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620", + "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82", + "sha256:11d9d9b87b50338b1b7de4ebf34fd29fdb0d219dc07ade29effc74d3d2609c62", + "sha256:15fdbe568beaca9aacfccd5ceadfb5f1a235087a127e8af5e48df9d8a45ae85c", + "sha256:19a3bd00b9dafc2cd7250d94d5b578edf7a0bd7daf102617153ff9a8fa37871c", + "sha256:23e8ec1ce4e57b4f441fc91e3c12adba023fedd06868445a5b5f1d48f0ab3682", + "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048", + "sha256:2ce3fcf75b2bae99aa31bd4968de0474ebe8c8258a0110903478bd83dfee4e3b", + "sha256:335a32d72c51a313b33fa3a9b0fe283503272ef6467910338e123f90925f0f03", + "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f", + "sha256:34a3613c7edb8c6fa578e58e9abe3c0f5e7430e0fc34a65a415a1683b9c32d9a", + "sha256:3d5492dbf953d7d849751917e3b2433fb26010d977aa7a0765c37425a4026ff1", + "sha256:44ae8a3e35a54d2e8fa88ed65e1b08967a9ef8c320819a969bfa09ce5528fafe", + "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33", + "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f", + "sha256:49e26c51ca854286bffc22b69787a8d4063a62bf7d83dc21d44d2ff426108518", + "sha256:65f7361a09b07915a98efd17fdec23103307a54db2000bb92095457ca758d485", + "sha256:6951f3f47cb5ca4da536ab161ac0163cab31417d20c54c6de5ddcab8bc813c3f", + "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec", + "sha256:74fe19dda960b193b0eb82c1f4d2c8e5e26918d9cda858cbf3f41dd28549cb70", + "sha256:7a4c5eec138a9b52c67f664c7d51d4c7234c5ad65dd8aacd919fb47445a62c86", + "sha256:80b982d42515632eb51f60fa1d217dfe0729f008e81a82d1544cc392e0a50ddf", + "sha256:941a2eb0a1509bd7f31e355912eb33b698eb0051730b2eaf9e70e2e1589cae1d", + "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588", + "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481", + "sha256:aad8771ec8dbf9139b01b56f66386537c6fe4e76c8f7a47c10261b69ad25c2c9", + "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3", + "sha256:b661ce52c7b5e5f600c0c3c5839e71918346af2ef20062705ae76b5c16914cab", + "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7", + "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a", + "sha256:c20f682defc9ef81cd7eaa485879ab29a86a0ba58acf669a78ed868e72bb89e0", + "sha256:c3e742f62198c9eb9201781fbebe64533a3bbf6a76a91b8d438d62b813079dbc", + "sha256:c5ae6b7c8483b1e0bf59e5f1843e4fd8fd405e11df7de217ee65b98eb5462861", + "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357", + "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a", + "sha256:d5389eb3b48a72da28c6e061a247ab224381435256eb541e175798483368fdd3", + "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80", + "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02", + "sha256:e9ee4e6ca1d9616797fa2e9c0bfb8815912c7d67aca96f77428e316741082a1b", + "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5", + "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2", + "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890", + "sha256:fe734914977eed33033b70bfc097e1baaffb589517863955430bf2e0846ac30f" + ], + "index": "mirror", + "version": "==1.10.18" + }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "index": "mirror", + "version": "==2.9.0.post0" + }, + "pyyaml": { + "hashes": [ + "sha256:0101357af42f5c9fc7e9acc5c5ab8c3049f50db7425de175b6c7a5959cb6023d", + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0ae563b7e3ed5e918cd0184060e28b48b7e672b975bf7c6f4a892cee9d886ada", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0fe2c1c5401a3a98f06337fed48f57340cf652a685484834b44f5ceeadb772ba", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1eb00dd3344da80264261ab126c95481824669ed9e5ecc82fb2d88b1fce668ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:2086b30215c433c1e480c08c1db8b43c1edd36c59cf43d36b424e6f35fcaf1ad", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:29b4a67915232f79506211e69943e3102e211c616181ceff0adf34e21b469357", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:2e9bc8a34797f0621f56160b961d47a088644370f79d34bedc934fb89e3f47dd", + "sha256:30ec6b9afc17353a9abcff109880edf6e8d5b924eb1eeed7fe9376febc1f9800", + "sha256:31573d7e161d2f905311f036b12e65c058389b474dbd35740f4880b91e2ca2be", + "sha256:36d7bf63558843ea2a81de9d0c3e9c56c353b1df8e6c1faaec86df5adedf2e02", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3af6b36bc195d741cd5b511810246cad143b99c953b4591e679e194a820d7b7c", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:414629800a1ddccd7303471650843fc801801cc579a195d2fe617b5b455409e3", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:459113f2b9cd68881201a3bd1a858ece3281dc0e92ece6e917d23b128f0fcb31", + "sha256:46e4fae38d00b40a62d32d60f1baa1b9ef33aff28c2aafd96b05d5cc770f1583", + "sha256:4bf821ccd51e8d5bc1a4021b8bd85a92b498832ac1cd1a53b399f0eb7c1c4258", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:50bd6560a6df3de59336b9a9086cbdea5aa9eee5361661448ee45c21eeb0da68", + "sha256:53056b51f111223e603bed1db5367f54596d44cacfa50f07e082a11929612957", + "sha256:53c5f0749a93e3296078262c9acf632de246241ff2f22bbedfe49d4b55e9bbdd", + "sha256:54c754cee6937bb9b72d6a16163160dec80b93a43020ac6fc9f13729c030c30b", + "sha256:58cc18ccbade0c48fb55102aa971a5b4e571e2b22187d083dda33f8708fa4ee7", + "sha256:5921fd128fbf27ab7c7ad1a566d2cd9557b84ade130743a7c110a55e7dec3b3c", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5c758cc29713c9166750a30156ca3d90ac2515d5dea3c874377ae8829cf03087", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:60bf91e73354c96754220a9c04a9502c2ad063231cd754b59f8e4511157e32e2", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:6f0f728a88c6eb58a3b762726b965bb6acf12d97f8ea2cb4fecf856a727f9bdc", + "sha256:6f31c5935310da69ea0efe996a962d488f080312f0eb43beff1717acb5fe9bed", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:728b447d0cedec409ea1a3f0ad1a6cc3cec0a8d086611b45f038a9230a2242f3", + "sha256:72ffbc5c0cc71877104387548a450f2b7b7c4926b40dc9443e7598fe92aa13d9", + "sha256:73d8b233309ecd45c33c51cd55aa1be1dcab1799a9e54f6c753d8cab054b8c34", + "sha256:765029d1cf96e9e761329ee1c20f1ca2de8644e7350a151b198260698b96e30f", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:7ee3d180d886a3bc50f753b76340f1c314f9e8c507f5b107212112214c3a66fd", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:826fb4d5ac2c48b9d6e71423def2669d4646c93b6c13612a71b3ac7bb345304b", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:84c39ceec517cd8f01cb144efb08904a32050be51c55b7a59bc7958c8091568d", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:88bfe675bb19ae12a9c77c52322a28a8e2a8d3d213fbcfcded5c3f5ca3ead352", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:8e0a1ebd5c5842595365bf90db3ef7e9a8d6a79c9aedb1d05b675c81c7267fd3", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9426067a10b369474396bf57fdf895b899045a25d1848798844693780b147436", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:9c5c0de7ec50d4df88b62f4b019ab7b3bb2883c826a1044268e9afb344c57b17", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:ad0c172fe15beffc32e3a8260f18e6708eb0e15ae82c9b3f80fbe04de0ef5729", + "sha256:ad206c7f5f08d393b872d3399f597246fdc6ebebff09c5ae5268ac45aebf4f8d", + "sha256:b0a163f4f84d1e0fe6a07ccad3b02e9b243790b8370ff0408ae5932c50c4d96d", + "sha256:b0dd9c7497d60126445e79e542ff01351c6b6dc121299d89787f5685b382c626", + "sha256:b1de10c488d6f02e498eb6956b89081bea31abf3133223c17749e7137734da75", + "sha256:b408f36eeb4e2be6f802f1be82daf1b578f3de5a51917c6e467aedb46187d827", + "sha256:bae077a01367e4bf5fddf00fd6c8b743e676385911c7c615e29e1c45ace8813b", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:bc3c3600fec6c2a719106381d6282061d8c108369cdec58b6f280610eba41e09", + "sha256:c16522bf91daa4ea9dedc1243b56b5a226357ab98b3133089ca627ef99baae6f", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:ca5136a77e2d64b4cf5106fb940376650ae232c74c09a8ff29dbb1e262495b31", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d6e0f7ee5f8d851b1d91149a3e5074dbf5aacbb63e4b771fcce16508339a856f", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:e7930a0612e74fcca37019ca851b50d73b5f0c3dab7f3085a7c15d2026118315", + "sha256:e8e6dd230a158a836cda3cc521fcbedea16f22b16b8cfa8054d0c6cea5d0a531", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:eee36bf4bc11e39e3f17c171f25cdedff3d7c73b148aedc8820257ce2aa56d3b", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f07adc282d51aaa528f3141ac1922d16d32fe89413ee59bfb8a73ed689ad3d23", + "sha256:f09816c047fdb588dddba53d321f1cb8081e38ad2a40ea6a7560a88b7a2f0ea8", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:fea4c4310061cd70ef73b39801231b9dc3dc638bb8858e38364b144fbd335a1a", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" + ], + "markers": "python_version >= '3.8'", + "version": "==6.0.2" + }, + "referencing": { + "hashes": [ + "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c", + "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de" + ], + "markers": "python_version >= '3.8'", + "version": "==0.35.1" + }, + "rpds-py": { + "hashes": [ + "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c", + "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585", + "sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5", + "sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6", + "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef", + "sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2", + "sha256:1e5f3cd7397c8f86c8cc72d5a791071431c108edd79872cdd96e00abd8497d29", + "sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318", + "sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b", + "sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399", + "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739", + "sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee", + "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174", + "sha256:338ca4539aad4ce70a656e5187a3a31c5204f261aef9f6ab50e50bcdffaf050a", + "sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344", + "sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2", + "sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03", + "sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5", + "sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22", + "sha256:40ce74fc86ee4645d0a225498d091d8bc61f39b709ebef8204cb8b5a464d3c0e", + "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96", + "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91", + "sha256:4b16aa0107ecb512b568244ef461f27697164d9a68d8b35090e9b0c1c8b27752", + "sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075", + "sha256:4fe84294c7019456e56d93e8ababdad5a329cd25975be749c3f5f558abb48253", + "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee", + "sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad", + "sha256:54b43a2b07db18314669092bb2de584524d1ef414588780261e31e85846c26a5", + "sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce", + "sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7", + "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b", + "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8", + "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57", + "sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3", + "sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec", + "sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209", + "sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921", + "sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045", + "sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074", + "sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580", + "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7", + "sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5", + "sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3", + "sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0", + "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24", + "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139", + "sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db", + "sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc", + "sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789", + "sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f", + "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2", + "sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c", + "sha256:a1862d2d7ce1674cffa6d186d53ca95c6e17ed2b06b3f4c476173565c862d232", + "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6", + "sha256:aa7f429242aae2947246587d2964fad750b79e8c233a2367f71b554e9447949c", + "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29", + "sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489", + "sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94", + "sha256:af0fc424a5842a11e28956e69395fbbeab2c97c42253169d87e90aac2886d751", + "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2", + "sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda", + "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9", + "sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51", + "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c", + "sha256:b8c00a3b1e70c1d3891f0db1b05292747f0dbcfb49c43f9244d04c70fbc40eb8", + "sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989", + "sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511", + "sha256:c3e130fd0ec56cb76eb49ef52faead8ff09d13f4527e9b0c400307ff72b408e1", + "sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2", + "sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150", + "sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c", + "sha256:ce9845054c13696f7af7f2b353e6b4f676dab1b4b215d7fe5e05c6f8bb06f965", + "sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f", + "sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58", + "sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b", + "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f", + "sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d", + "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821", + "sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de", + "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121", + "sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855", + "sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272", + "sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60", + "sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02", + "sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1", + "sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140", + "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879", + "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940", + "sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364", + "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4", + "sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e", + "sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420", + "sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5", + "sha256:f2fbf7db2012d4876fb0d66b5b9ba6591197b0f165db8d99371d976546472a24", + "sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c", + "sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf", + "sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f", + "sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e", + "sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab", + "sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08", + "sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92", + "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a", + "sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8" + ], + "markers": "python_version >= '3.8'", + "version": "==0.20.0" + }, + "setuptools": { + "hashes": [ + "sha256:753bb6ebf1f465a1912e19ed1d41f403a79173a9acf66a42e7e6aec45c3c16ec", + "sha256:a7fcb66f68b4d9e8e66b42f9876150a3371558f98fa32222ffaa5bced76406f8" + ], + "index": "mirror", + "version": "==75.2.0" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "sqlparse": { + "hashes": [ + "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", + "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e" + ], + "markers": "python_version >= '3.8'", + "version": "==0.5.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" + ], + "markers": "python_version >= '3.8'", + "version": "==4.12.2" + }, + "uritemplate": { + "hashes": [ + "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0", + "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e" + ], + "markers": "python_version >= '3.6'", + "version": "==4.1.1" + }, + "urllib3": { + "hashes": [ + "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" + ], + "markers": "python_version >= '3.8'", + "version": "==2.2.3" + }, + "whitenoise": { + "hashes": [ + "sha256:58c7a6cd811e275a6c91af22e96e87da0b1109e9a53bb7464116ef4c963bf636", + "sha256:a1ae85e01fdc9815d12fa33f17765bc132ed2c54fa76daf9e39e879dd93566f6" + ], + "index": "mirror", + "version": "==6.7.0" + } + }, + "develop": { + "asgiref": { + "hashes": [ + "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", + "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590" + ], + "markers": "python_version >= '3.8'", + "version": "==3.8.1" + }, + "astor": { + "hashes": [ + "sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5", + "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.8.1" + }, + "asttokens": { + "hashes": [ + "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24", + "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0" + ], + "version": "==2.4.1" + }, + "attrs": { + "hashes": [ + "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", + "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2" + ], + "markers": "python_version >= '3.7'", + "version": "==24.2.0" + }, + "backcall": { + "hashes": [ + "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", + "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255" + ], + "version": "==0.2.0" + }, + "bandit": { + "hashes": [ + "sha256:17e60786a7ea3c9ec84569fd5aee09936d116cb0cb43151023258340dbffb7ed", + "sha256:527906bec6088cb499aae31bc962864b4e77569e9d529ee51df3a93b4b8ab28a" + ], + "index": "mirror", + "version": "==1.7.7" + }, + "black": { + "hashes": [ + "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f", + "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93", + "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11", + "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0", + "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9", + "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5", + "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213", + "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d", + "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7", + "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837", + "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f", + "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395", + "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995", + "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f", + "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597", + "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959", + "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5", + "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb", + "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4", + "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7", + "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd", + "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7" + ], + "index": "mirror", + "version": "==24.3.0" + }, + "certifi": { + "hashes": [ + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" + ], + "markers": "python_version >= '3.6'", + "version": "==2024.8.30" + }, + "charset-normalizer": { + "hashes": [ + "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", + "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", + "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", + "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", + "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", + "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", + "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", + "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", + "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", + "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", + "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", + "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", + "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", + "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", + "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", + "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", + "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", + "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", + "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", + "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", + "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", + "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", + "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", + "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", + "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", + "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", + "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", + "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", + "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", + "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", + "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", + "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", + "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", + "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", + "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", + "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", + "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", + "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", + "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", + "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", + "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", + "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", + "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", + "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", + "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", + "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", + "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", + "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", + "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", + "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", + "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", + "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", + "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", + "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", + "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", + "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", + "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", + "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", + "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", + "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", + "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", + "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", + "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", + "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", + "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", + "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", + "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", + "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", + "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", + "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", + "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", + "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", + "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", + "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", + "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", + "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", + "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", + "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", + "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", + "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", + "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", + "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", + "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", + "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", + "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", + "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", + "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", + "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", + "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", + "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", + "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", + "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", + "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", + "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", + "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", + "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", + "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", + "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", + "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", + "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", + "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", + "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", + "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", + "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", + "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.4.0" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", + "sha256:96e0137fb3ab6b56576b4638116d77c59f3e0565f4ea081172e4721c722afa92", + "sha256:bc3a1efa0b297242dcd0757e2e83d358bcd18bda77735e493aa89a634e74c9bf" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==0.4.6" + }, + "coreapi": { + "hashes": [ + "sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb", + "sha256:bf39d118d6d3e171f10df9ede5666f63ad80bba9a29a8ec17726a66cf52ee6f3" + ], + "version": "==2.3.3" + }, + "coreschema": { + "hashes": [ + "sha256:5e6ef7bf38c1525d5e55a895934ab4273548629f16aed5c0a6caa74ebf45551f", + "sha256:9503506007d482ab0867ba14724b93c18a33b22b6d19fb419ef2d239dd4a1607" + ], + "version": "==0.0.4" + }, + "coverage": { + "hashes": [ + "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79", + "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a", + "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f", + "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a", + "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa", + "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398", + "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba", + "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d", + "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf", + "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b", + "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518", + "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d", + "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795", + "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2", + "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e", + "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32", + "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745", + "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b", + "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e", + "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d", + "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f", + "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660", + "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62", + "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6", + "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04", + "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c", + "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5", + "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef", + "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc", + "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae", + "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578", + "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466", + "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4", + "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91", + "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0", + "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4", + "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b", + "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe", + "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b", + "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75", + "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b", + "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c", + "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72", + "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b", + "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f", + "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e", + "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53", + "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3", + "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84", + "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987" + ], + "index": "mirror", + "version": "==6.5.0" + }, + "decorator": { + "hashes": [ + "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", + "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186" + ], + "markers": "python_version >= '3.7'", + "version": "==5.1.1" + }, + "django": { + "hashes": [ + "sha256:1ddc333a16fc139fd253035a1606bb24261951bbc3a6ca256717fa06cc41a898", + "sha256:6f1616c2786c408ce86ab7e10f792b8f15742f7b7b7460243929cb371e7f1dad" + ], + "index": "mirror", + "version": "==4.2.16" + }, + "django-stubs": { + "hashes": [ + "sha256:59c9f81af64d214b1954eaf90f037778c8d2b9c2de946a3cda177fefcf588fbd", + "sha256:664843091636a917faf5256d028476559dc360fdef9050b6df87ab61b21607bf" + ], + "index": "mirror", + "version": "==1.9.0" + }, + "django-stubs-ext": { + "hashes": [ + "sha256:a455fc222c90b30b29ad8c53319559f5b54a99b4197205ddbb385aede03b395d", + "sha256:ed7d51c0b731651879fc75f331fb0806d98b67bfab464e96e2724db6b46ef926" + ], + "markers": "python_version >= '3.8'", + "version": "==5.1.0" + }, + "djangorestframework-stubs": { + "hashes": [ + "sha256:037f0582b1e6c79366b6a839da861474d59210c4bfa1d36291545cb6ede6a0da", + "sha256:f6ed5fb19c12aa752288ddc6ad28d4ca7c81681ca7f28a19aba9064b2a69489c" + ], + "index": "mirror", + "version": "==1.4.0" + }, + "dlint": { + "hashes": [ + "sha256:344823d299439aa94fe276b2b3b90733026787d25713c664e137cf5f7d0645f7" + ], + "index": "mirror", + "version": "==0.12.0" + }, + "dparse": { + "hashes": [ + "sha256:0d8fe18714056ca632d98b24fbfc4e9791d4e47065285ab486182288813a5318", + "sha256:27bb8b4bcaefec3997697ba3f6e06b2447200ba273c0b085c3d012a04571b528" + ], + "markers": "python_version >= '3.6'", + "version": "==0.6.3" + }, + "entrypoints": { + "hashes": [ + "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4", + "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f" + ], + "markers": "python_version >= '3.6'", + "version": "==0.4" + }, + "eradicate": { + "hashes": [ + "sha256:06df115be3b87d0fc1c483db22a2ebb12bcf40585722810d809cc770f5031c37", + "sha256:2b29b3dd27171f209e4ddd8204b70c02f0682ae95eecb353f10e8d72b149c63e" + ], + "version": "==2.3.0" + }, + "exceptiongroup": { + "hashes": [ + "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", + "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc" + ], + "markers": "python_version < '3.11'", + "version": "==1.2.2" + }, + "executing": { + "hashes": [ + "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf", + "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab" + ], + "markers": "python_version >= '3.8'", + "version": "==2.1.0" + }, + "factory-boy": { + "hashes": [ + "sha256:a98d277b0c047c75eb6e4ab8508a7f81fb03d2cb21986f627913546ef7a2a55e", + "sha256:eb02a7dd1b577ef606b75a253b9818e6f9eaf996d94449c9d5ebb124f90dc795" + ], + "index": "mirror", + "version": "==3.2.1" + }, + "faker": { + "hashes": [ + "sha256:66db859b6abe376d02e805ad81eb8dcfce38f0945f17ee7cdf74ed349985ea52", + "sha256:fe969607836ce7100e38b88dcb598aacb733d895e6e9401894dd603e35623000" + ], + "index": "mirror", + "version": "==13.3.2" + }, + "flake8": { + "hashes": [ + "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d", + "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d" + ], + "index": "mirror", + "version": "==4.0.1" + }, + "flake8-annotations": { + "hashes": [ + "sha256:3edfbbfb58e404868834fe6ec3eaf49c139f64f0701259f707d043185545151e", + "sha256:52e53c05b0c06cac1c2dec192ea2c36e85081238add3bd99421d56f574b9479b" + ], + "index": "mirror", + "version": "==2.7.0" + }, + "flake8-bugbear": { + "hashes": [ + "sha256:152e64a86f6bff6e295d630ccc993f62434c1fd2b20d2fae47547cb1c1b868e0", + "sha256:19fe179ee3286e16198603c438788e2949e79f31d653f0bdb56d53fb69217bd0" + ], + "index": "mirror", + "version": "==22.3.20" + }, + "flake8-comprehensions": { + "hashes": [ + "sha256:8e108707637b1d13734f38e03435984f6b7854fa6b5a4e34f93e69534be8e521", + "sha256:9406314803abe1193c064544ab14fdc43c58424c0882f6ff8a581eb73fc9bb58" + ], + "index": "mirror", + "version": "==3.8.0" + }, + "flake8-docstrings": { + "hashes": [ + "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde", + "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b" + ], + "index": "mirror", + "version": "==1.6.0" + }, + "flake8-eradicate": { + "hashes": [ + "sha256:51dc660d0c1c1ed93af0f813540bbbf72ab2d3466c14e3f3bac371c618b6042f", + "sha256:acaa1b6839ff00d284b805c432fdfa6047262bd15a5504ec945797e87b4de1fa" + ], + "index": "mirror", + "version": "==1.2.0" + }, + "flake8-plugin-utils": { + "hashes": [ + "sha256:39f6f338d038b301c6fd344b06f2e81e382b68fa03c0560dff0d9b1791a11a2c", + "sha256:e4848c57d9d50f19100c2d75fa794b72df068666a9041b4b0409be923356a3ed" + ], + "markers": "python_version >= '3.6' and python_version < '4.0'", + "version": "==1.3.3" + }, + "flake8-polyfill": { + "hashes": [ + "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9", + "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda" + ], + "version": "==1.0.2" + }, + "flake8-pytest": { + "hashes": [ + "sha256:61686128a79e1513db575b2bcac351081d5a293811ddce2d5dfc25e8c762d33e", + "sha256:b4d6703f7d7b646af1e2660809e795886dd349df11843613dbe6515efa82c0f3" + ], + "index": "mirror", + "version": "==1.3" + }, + "flake8-pytest-style": { + "hashes": [ + "sha256:5fedb371a950e9fe0e0e6bfc854be7d99151271208f34cd2cc517681ece27780", + "sha256:c1175713e9e11b78cd1a035ed0bca0d1e41d09c4af329a952750b61d4194ddac" + ], + "index": "mirror", + "version": "==1.6.0" + }, + "flake8-simplify": { + "hashes": [ + "sha256:8344a6a77b214ad7b3559d06a42c97333b73b11034abb018ea02cebba9fdebf1", + "sha256:a4a0fcaad8fa0ad17634c6fa4ff62ebe4835bef48d78d3104eb528140cf0972b" + ], + "index": "mirror", + "version": "==0.18.1" + }, + "flake8-use-fstring": { + "hashes": [ + "sha256:1bd4a409adbb93e64e711fdd26b88759c33792e3899f174edc68ddf7307e81b6" + ], + "index": "mirror", + "version": "==1.3" + }, + "flake8-variables-names": { + "hashes": [ + "sha256:30133e14ee2300e13a60393a00f74d98110c76070ac67d1ab91606f02824a7e1", + "sha256:e3277031696bbe10b5132b49938cde1d70fcae9561533b7bd7ab8e69cb27addb" + ], + "index": "mirror", + "version": "==0.0.5" + }, + "flakeheaven": { + "hashes": [ + "sha256:20f5a573b8e85be5a73fed2e3967f7ab8b5e77714a5898d8b634dd31a0b75f9b", + "sha256:5614165d95bf26c46af22db4e0ccc90e475c286f902c473de16730fad4735e5d" + ], + "index": "mirror", + "version": "==0.11.1" + }, + "freezegun": { + "hashes": [ + "sha256:15103a67dfa868ad809a8f508146e396be2995172d25f927e48ce51c0bf5cb09", + "sha256:b4c64efb275e6bc68dc6e771b17ffe0ff0f90b81a2a5189043550b6519926ba4" + ], + "index": "mirror", + "version": "==1.2.1" + }, + "future": { + "hashes": [ + "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", + "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.0.0" + }, + "hypothesis": { + "hashes": [ + "sha256:41503e20a246ab4522d78f2df8afc40fd7349eeaf0fe07cdc233069c671e6e35", + "sha256:f60b1dfaa8c2175c40513449f9c49b7543d50e66e16a5e22cf5fca460e864037" + ], + "index": "mirror", + "version": "==6.39.4" + }, + "idna": { + "hashes": [ + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" + ], + "markers": "python_version >= '3.6'", + "version": "==3.10" + }, + "importlab": { + "hashes": [ + "sha256:124cfa00e8a34fefe8aac1a5e94f56c781b178c9eb61a1d3f60f7e03b77338d3", + "sha256:b3893853b1f6eb027da509c3b40e6787e95dd66b4b66f1b3613aad77556e1465" + ], + "markers": "python_full_version >= '3.6.0'", + "version": "==0.8.1" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "ipdb": { + "hashes": [ + "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5" + ], + "index": "mirror", + "version": "==0.13.9" + }, + "ipython": { + "hashes": [ + "sha256:b13a1d6c1f5818bd388db53b7107d17454129a70de2b87481d555daede5eb49e", + "sha256:b38c31e8fc7eff642fc7c597061fff462537cf2314e3225a19c906b7b0d8a345" + ], + "index": "mirror", + "version": "==8.10.0" + }, + "isort": { + "hashes": [ + "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", + "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951" + ], + "index": "mirror", + "version": "==5.10.1" + }, + "itypes": { + "hashes": [ + "sha256:03da6872ca89d29aef62773672b2d408f490f80db48b23079a4b194c86dd04c6", + "sha256:af886f129dea4a2a1e3d36595a2d139589e4dd287f5cab0b40e799ee81570ff1" + ], + "version": "==1.2.0" + }, + "jedi": { + "hashes": [ + "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd", + "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0" + ], + "markers": "python_version >= '3.6'", + "version": "==0.19.1" + }, + "jinja2": { + "hashes": [ + "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", + "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.4" + }, + "libcst": { + "hashes": [ + "sha256:02be4aab728261bb76d16e77c9a457884cebb60d09c8edee844de43b0e08aff7", + "sha256:208ea92d80b2eeed8cbc879d5f39f241582a5d56b916b1b65ed2be2f878a2425", + "sha256:23d0e07fd3ed11480f8993a1e99d58a45f914a711b14f858b8db08ae861a8a34", + "sha256:2d5978fd60c66794bb60d037b2e6427ea52d032636e84afce32b0f04e1cf500a", + "sha256:40748361f4ea66ab6cdd82f8501c82c29808317ac7a3bd132074efd5fd9bfae2", + "sha256:48e581af6127c5af4c9f483e5986d94f0c6b2366967ee134f0a8eba0aa4c8c12", + "sha256:4d6acb0bdee1e55b44c6215c59755ec4693ac01e74bb1fde04c37358b378835d", + "sha256:4f71aed85932c2ea92058fd9bbd99a6478bd69eada041c3726b4f4c9af1f564e", + "sha256:52b6aadfe54e3ae52c3b815eaaa17ba4da9ff010d5e8adf6a70697872886dd10", + "sha256:585b3aa705b3767d717d2100935d8ef557275ecdd3fac81c3e28db0959efb0ea", + "sha256:5f10124bf99a0b075eae136ef0ce06204e5f6b8da4596a9c4853a0663e80ddf3", + "sha256:6453b5a8755a6eee3ad67ee246f13a8eac9827d2cfc8e4a269e8bf0393db74bc", + "sha256:6fb324ed20f3a725d152df5dba8d80f7e126d9c93cced581bf118a5fc18c1065", + "sha256:7dba93cca0a5c6d771ed444c44d21ce8ea9b277af7036cea3743677aba9fbbb8", + "sha256:80b5c4d87721a7bab265c202575809b810815ab81d5e2e7a5d4417a087975840", + "sha256:83bc5fbe34d33597af1d5ea113dcb9b5dd5afe5a5f4316bac4293464d5e3971a", + "sha256:8478abf21ae3861a073e898d80b822bd56e578886331b33129ba77fec05b8c24", + "sha256:88520b6dea59eaea0cae80f77c0a632604a82c5b2d23dedb4b5b34035cbf1615", + "sha256:8935dd3393e30c2f97344866a4cb14efe560200e232166a8db1de7865c2ef8b2", + "sha256:96adc45e96476350df6b8a5ddbb1e1d6a83a7eb3f13087e52eb7cd2f9b65bcc7", + "sha256:99e7c52150a135d66716b03e00c7b1859a44336dc2a2bf8f9acc164494308531", + "sha256:9cccfc0a78e110c0d0a9d2c6fdeb29feb5274c9157508a8baef7edf352420f6d", + "sha256:a8fcd78be4d9ce3c36d0c5d0bdd384e0c7d5f72970a9e4ebd56070141972b4ad", + "sha256:b48bf71d52c1e891a0948465a94d9817b5fc1ec1a09603566af90585f3b11948", + "sha256:b5b5bcd3a9ba92840f27ad34eaa038acbee195ec337da39536c0a2efbbf28efd", + "sha256:b60b09abcc2848ab52d479c3a9b71b606d91a941e3779616efd083bb87dbe8ad", + "sha256:d2788b2b5838b78fe15df8e9fa6b6903195ea49b2d2ba43e8f423f6c90e4b69f", + "sha256:d4592872aaf5b7fa5c2727a7d73c0985261f1b3fe7eff51f4fd5b8174f30b4e2", + "sha256:d6502aeb11412afc759036160c686be1107eb5a4466db56b207c786b9b4da7c4", + "sha256:d92c5ae2e2dc9356ad7e3d05077d9b7e5065423e45788fd86729c88729e45c6e", + "sha256:fc80ea16c7d44e38f193e4d4ef7ff1e0ba72d8e60e8b61ac6f4c87f070a118bd" + ], + "markers": "python_version >= '3.9'", + "version": "==1.5.0" + }, + "mando": { + "hashes": [ + "sha256:4ce09faec7e5192ffc3c57830e26acba0fd6cd11e1ee81af0d4df0657463bd1c", + "sha256:79feb19dc0f097daa64a1243db578e7674909b75f88ac2220f1c065c10a0d960" + ], + "version": "==0.6.4" + }, + "markdown-it-py": { + "hashes": [ + "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", + "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" + ], + "markers": "python_version >= '3.8'", + "version": "==3.0.0" + }, + "markupsafe": { + "hashes": [ + "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", + "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", + "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", + "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", + "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", + "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", + "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", + "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", + "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", + "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", + "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", + "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", + "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", + "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", + "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", + "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", + "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", + "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", + "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", + "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", + "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", + "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", + "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", + "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", + "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", + "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", + "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", + "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", + "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", + "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", + "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", + "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", + "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", + "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", + "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", + "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", + "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", + "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", + "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", + "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", + "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", + "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", + "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", + "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", + "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", + "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", + "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", + "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", + "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", + "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", + "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", + "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", + "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", + "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", + "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", + "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", + "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", + "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", + "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", + "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", + "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" + ], + "markers": "python_version >= '3.9'", + "version": "==3.0.2" + }, + "matplotlib-inline": { + "hashes": [ + "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", + "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca" + ], + "markers": "python_version >= '3.8'", + "version": "==0.1.7" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "mdurl": { + "hashes": [ + "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" + ], + "markers": "python_version >= '3.7'", + "version": "==0.1.2" + }, + "mypy": { + "hashes": [ + "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36", + "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce", + "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6", + "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b", + "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca", + "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24", + "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383", + "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7", + "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86", + "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d", + "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4", + "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8", + "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987", + "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385", + "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79", + "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef", + "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6", + "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70", + "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca", + "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70", + "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12", + "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104", + "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a", + "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318", + "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1", + "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b", + "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d" + ], + "index": "mirror", + "version": "==1.11.2" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "index": "mirror", + "version": "==1.0.0" + }, + "networkx": { + "hashes": [ + "sha256:26c93556ec69ce960d9f392d162f8dce551108980870f70f41e0d2001625b747", + "sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36", + "sha256:9631418f952dd7c2b072b9768316885b137592243436b3784c18078d48bb29af", + "sha256:a451926de99506c82cccf81c4ea17bd996520c8513bd1479fe28faaa521909a9", + "sha256:a6234c7cef798384a839fbead8acd202e74b335efe525619caada4d69a95c8e2", + "sha256:afdee186a6266a6f786c9c88a3aea787bf8ff56a04b68dda92e5921e175c729b", + "sha256:bba8140e5e3a84d16d60da5e8e4ae201f3faddfff39bf8a1402c15c457ecbbf1", + "sha256:c5ea67a4f9b2f62e0b8d464949a8117f371e55195e3b538660e51f0a813da48a", + "sha256:c75bdeb795c7a8be4e8910e793fda7cc969180a8156a7f221fa094fd77f798aa", + "sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61", + "sha256:ee16453c716afe5fb8abd487f951d789cd248d411f9950ba46146dab96a35668", + "sha256:eea068609d3c0c11cfc52fcb29b70bcd7860a1ca830d3a6c87ce13df0055c90c" + ], + "markers": "python_version >= '3.8'", + "version": "==3.1" + }, + "ninja": { + "hashes": [ + "sha256:18302d96a5467ea98b68e1cae1ae4b4fb2b2a56a82b955193c637557c7273dbd", + "sha256:185e0641bde601e53841525c4196278e9aaf4463758da6dd1e752c0a0f54136a", + "sha256:376889c76d87b95b5719fdd61dd7db193aa7fd4432e5d52d2e44e4c497bdbbee", + "sha256:3e0f9be5bb20d74d58c66cc1c414c3e6aeb45c35b0d0e41e8d739c2c0d57784f", + "sha256:73b93c14046447c7c5cc892433d4fae65d6364bec6685411cb97a8bcf815f93a", + "sha256:7563ce1d9fe6ed5af0b8dd9ab4a214bf4ff1f2f6fd6dc29f480981f0f8b8b249", + "sha256:76482ba746a2618eecf89d5253c0d1e4f1da1270d41e9f54dfbd91831b0f6885", + "sha256:84502ec98f02a037a169c4b0d5d86075eaf6afc55e1879003d6cab51ced2ea4b", + "sha256:95da904130bfa02ea74ff9c0116b4ad266174fafb1c707aa50212bc7859aebf1", + "sha256:9d793b08dd857e38d0b6ffe9e6b7145d7c485a42dcfea04905ca0cdb6017cc3c", + "sha256:9df724344202b83018abb45cb1efc22efd337a1496514e7e6b3b59655be85205", + "sha256:aad34a70ef15b12519946c5633344bc775a7656d789d9ed5fdb0d456383716ef", + "sha256:d491fc8d89cdcb416107c349ad1e3a735d4c4af5e1cb8f5f727baca6350fdaea", + "sha256:ecf80cf5afd09f14dcceff28cb3f11dc90fb97c999c89307aea435889cb66877", + "sha256:fa2ba9d74acfdfbfbcf06fad1b8282de8a7a8c481d9dee45c859a8c93fcc1082" + ], + "version": "==1.11.1.1" + }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, + "parso": { + "hashes": [ + "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", + "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d" + ], + "markers": "python_version >= '3.6'", + "version": "==0.8.4" + }, + "pathspec": { + "hashes": [ + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "pbr": { + "hashes": [ + "sha256:788183e382e3d1d7707db08978239965e8b9e4e5ed42669bf4758186734d5f24", + "sha256:a776ae228892d8013649c0aeccbb3d5f99ee15e005a4cbb7e61d55a067b28a2a" + ], + "markers": "python_version >= '2.6'", + "version": "==6.1.0" + }, + "pep8-naming": { + "hashes": [ + "sha256:4a8daeaeb33cfcde779309fc0c9c0a68a3bbe2ad8a8308b763c5068f86eb9f37", + "sha256:bb2455947757d162aa4cad55dba4ce029005cd1692f2899a21d51d8630ca7841" + ], + "index": "mirror", + "version": "==0.12.1" + }, + "pexpect": { + "hashes": [ + "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", + "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f" + ], + "markers": "sys_platform != 'win32'", + "version": "==4.9.0" + }, + "pickleshare": { + "hashes": [ + "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", + "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" + ], + "version": "==0.7.5" + }, + "platformdirs": { + "hashes": [ + "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", + "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb" + ], + "markers": "python_version >= '3.8'", + "version": "==4.3.6" + }, + "pluggy": { + "hashes": [ + "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" + }, + "prompt-toolkit": { + "hashes": [ + "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90", + "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.0.48" + }, + "ptyprocess": { + "hashes": [ + "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", + "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" + ], + "version": "==0.7.0" + }, + "pure-eval": { + "hashes": [ + "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", + "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42" + ], + "version": "==0.2.3" + }, + "pycnite": { + "hashes": [ + "sha256:5125f1c95aef4a23b9bec3b32fae76873dcd46324fa68e39c10fa852ecdea340", + "sha256:9ff9c09d35056435b867e14ebf79626ca94b6017923a0bf9935377fa90d4cbb3" + ], + "markers": "python_version >= '3.8'", + "version": "==2024.7.31" + }, + "pycodestyle": { + "hashes": [ + "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20", + "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==2.8.0" + }, + "pydocstyle": { + "hashes": [ + "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019", + "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1" + ], + "markers": "python_version >= '3.6'", + "version": "==6.3.0" + }, + "pydot": { + "hashes": [ + "sha256:9180da540b51b3aa09fbf81140b3edfbe2315d778e8589a7d0a4a69c41332bae", + "sha256:99cedaa55d04abb0b2bc56d9981a6da781053dd5ac75c428e8dd53db53f90b14" + ], + "markers": "python_version >= '3.8'", + "version": "==3.0.2" + }, + "pyflakes": { + "hashes": [ + "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c", + "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.4.0" + }, + "pygments": { + "hashes": [ + "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", + "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" + ], + "markers": "python_version >= '3.8'", + "version": "==2.18.0" + }, + "pyparsing": { + "hashes": [ + "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", + "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c" + ], + "markers": "python_version >= '3.9'", + "version": "==3.2.0" + }, + "pytest": { + "hashes": [ + "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", + "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" + ], + "index": "mirror", + "version": "==8.3.3" + }, + "pytest-cov": { + "hashes": [ + "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", + "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857" + ], + "index": "mirror", + "version": "==5.0.0" + }, + "pytest-django": { + "hashes": [ + "sha256:1d83692cb39188682dbb419ff0393867e9904094a549a7d38a3154d5731b2b99", + "sha256:8bf7bc358c9ae6f6fc51b6cebb190fe20212196e6807121f11bd6a3b03428314" + ], + "index": "mirror", + "version": "==4.9.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "index": "mirror", + "version": "==2.9.0.post0" + }, + "pytype": { + "hashes": [ + "sha256:0e59fb2866cdd804b31ef9baa0f53bc6fddaeaf68a7b1af6aa62c1b9854f017e", + "sha256:144e83bb7b80e5b0972a25bed2e4f03c0cd91d677ad0902bb775fdac156151ca", + "sha256:20680085e5a7beee2aaedddbf96863efa96ff047061ae82e31404336180502f5", + "sha256:33227cd847df1c5e92dbe013dc3926b78c77b36032a1caa3e488286aa9a0a14e", + "sha256:5281cc89ba5acc5a9184845f5c02319c0fbfcd87a9ab4920b6bce0d0caec6860", + "sha256:734d34b3ce13ccea64c419f88630fd189d6d06cbb24516cfb6fe36edc1860da2", + "sha256:a964a105af46fff3495be76ee34ad34acf47300018244122ea75d0e777d04cb5", + "sha256:af4b7ced6049e7fececb646262a25874e8a4e6a0b7e540c100b673aacd230ffa", + "sha256:b1b802867ede7cfd7dbe479cfdd3a1341dac005bfcb2718ff22070494fdd57be", + "sha256:b1c5857acd6348e9f5ace6427d74824f433d6bbfad0e0104b98f5078f5781af6", + "sha256:bf28233b140e9a7702cffcc8346c8d47b492813bee5f11ba2cef906ff4c05c55", + "sha256:c22db76f45a218c673f70c4ab32e7de757b808ce5d9ae55d1b3621f05187c496" + ], + "index": "mirror", + "version": "==2024.01.05" + }, + "pyyaml": { + "hashes": [ + "sha256:0101357af42f5c9fc7e9acc5c5ab8c3049f50db7425de175b6c7a5959cb6023d", + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0ae563b7e3ed5e918cd0184060e28b48b7e672b975bf7c6f4a892cee9d886ada", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0fe2c1c5401a3a98f06337fed48f57340cf652a685484834b44f5ceeadb772ba", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1eb00dd3344da80264261ab126c95481824669ed9e5ecc82fb2d88b1fce668ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:2086b30215c433c1e480c08c1db8b43c1edd36c59cf43d36b424e6f35fcaf1ad", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:29b4a67915232f79506211e69943e3102e211c616181ceff0adf34e21b469357", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:2e9bc8a34797f0621f56160b961d47a088644370f79d34bedc934fb89e3f47dd", + "sha256:30ec6b9afc17353a9abcff109880edf6e8d5b924eb1eeed7fe9376febc1f9800", + "sha256:31573d7e161d2f905311f036b12e65c058389b474dbd35740f4880b91e2ca2be", + "sha256:36d7bf63558843ea2a81de9d0c3e9c56c353b1df8e6c1faaec86df5adedf2e02", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3af6b36bc195d741cd5b511810246cad143b99c953b4591e679e194a820d7b7c", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:414629800a1ddccd7303471650843fc801801cc579a195d2fe617b5b455409e3", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:459113f2b9cd68881201a3bd1a858ece3281dc0e92ece6e917d23b128f0fcb31", + "sha256:46e4fae38d00b40a62d32d60f1baa1b9ef33aff28c2aafd96b05d5cc770f1583", + "sha256:4bf821ccd51e8d5bc1a4021b8bd85a92b498832ac1cd1a53b399f0eb7c1c4258", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:50bd6560a6df3de59336b9a9086cbdea5aa9eee5361661448ee45c21eeb0da68", + "sha256:53056b51f111223e603bed1db5367f54596d44cacfa50f07e082a11929612957", + "sha256:53c5f0749a93e3296078262c9acf632de246241ff2f22bbedfe49d4b55e9bbdd", + "sha256:54c754cee6937bb9b72d6a16163160dec80b93a43020ac6fc9f13729c030c30b", + "sha256:58cc18ccbade0c48fb55102aa971a5b4e571e2b22187d083dda33f8708fa4ee7", + "sha256:5921fd128fbf27ab7c7ad1a566d2cd9557b84ade130743a7c110a55e7dec3b3c", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5c758cc29713c9166750a30156ca3d90ac2515d5dea3c874377ae8829cf03087", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:60bf91e73354c96754220a9c04a9502c2ad063231cd754b59f8e4511157e32e2", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:6f0f728a88c6eb58a3b762726b965bb6acf12d97f8ea2cb4fecf856a727f9bdc", + "sha256:6f31c5935310da69ea0efe996a962d488f080312f0eb43beff1717acb5fe9bed", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:728b447d0cedec409ea1a3f0ad1a6cc3cec0a8d086611b45f038a9230a2242f3", + "sha256:72ffbc5c0cc71877104387548a450f2b7b7c4926b40dc9443e7598fe92aa13d9", + "sha256:73d8b233309ecd45c33c51cd55aa1be1dcab1799a9e54f6c753d8cab054b8c34", + "sha256:765029d1cf96e9e761329ee1c20f1ca2de8644e7350a151b198260698b96e30f", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:7ee3d180d886a3bc50f753b76340f1c314f9e8c507f5b107212112214c3a66fd", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:826fb4d5ac2c48b9d6e71423def2669d4646c93b6c13612a71b3ac7bb345304b", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:84c39ceec517cd8f01cb144efb08904a32050be51c55b7a59bc7958c8091568d", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:88bfe675bb19ae12a9c77c52322a28a8e2a8d3d213fbcfcded5c3f5ca3ead352", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:8e0a1ebd5c5842595365bf90db3ef7e9a8d6a79c9aedb1d05b675c81c7267fd3", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9426067a10b369474396bf57fdf895b899045a25d1848798844693780b147436", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:9c5c0de7ec50d4df88b62f4b019ab7b3bb2883c826a1044268e9afb344c57b17", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:ad0c172fe15beffc32e3a8260f18e6708eb0e15ae82c9b3f80fbe04de0ef5729", + "sha256:ad206c7f5f08d393b872d3399f597246fdc6ebebff09c5ae5268ac45aebf4f8d", + "sha256:b0a163f4f84d1e0fe6a07ccad3b02e9b243790b8370ff0408ae5932c50c4d96d", + "sha256:b0dd9c7497d60126445e79e542ff01351c6b6dc121299d89787f5685b382c626", + "sha256:b1de10c488d6f02e498eb6956b89081bea31abf3133223c17749e7137734da75", + "sha256:b408f36eeb4e2be6f802f1be82daf1b578f3de5a51917c6e467aedb46187d827", + "sha256:bae077a01367e4bf5fddf00fd6c8b743e676385911c7c615e29e1c45ace8813b", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:bc3c3600fec6c2a719106381d6282061d8c108369cdec58b6f280610eba41e09", + "sha256:c16522bf91daa4ea9dedc1243b56b5a226357ab98b3133089ca627ef99baae6f", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:ca5136a77e2d64b4cf5106fb940376650ae232c74c09a8ff29dbb1e262495b31", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d6e0f7ee5f8d851b1d91149a3e5074dbf5aacbb63e4b771fcce16508339a856f", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:e7930a0612e74fcca37019ca851b50d73b5f0c3dab7f3085a7c15d2026118315", + "sha256:e8e6dd230a158a836cda3cc521fcbedea16f22b16b8cfa8054d0c6cea5d0a531", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:eee36bf4bc11e39e3f17c171f25cdedff3d7c73b148aedc8820257ce2aa56d3b", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f07adc282d51aaa528f3141ac1922d16d32fe89413ee59bfb8a73ed689ad3d23", + "sha256:f09816c047fdb588dddba53d321f1cb8081e38ad2a40ea6a7560a88b7a2f0ea8", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:fea4c4310061cd70ef73b39801231b9dc3dc638bb8858e38364b144fbd335a1a", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" + ], + "markers": "python_version >= '3.8'", + "version": "==6.0.2" + }, + "radon": { + "hashes": [ + "sha256:cb1d8752e5f862fb9e20d82b5f758cbc4fb1237c92c9a66450ea0ea7bf29aeee", + "sha256:fa74e018197f1fcb54578af0f675d8b8e2342bd8e0b72bef8197bc4c9e645f36" + ], + "index": "mirror", + "version": "==5.1.0" + }, + "requests": { + "hashes": [ + "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" + ], + "markers": "python_version >= '3.8'", + "version": "==2.32.3" + }, + "rich": { + "hashes": [ + "sha256:9836f5096eb2172c9e77df411c1b009bace4193d6a481d534fea75ebba758283", + "sha256:bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==13.9.3" + }, + "ruamel.yaml": { + "hashes": [ + "sha256:57b53ba33def16c4f3d807c0ccbc00f8a6081827e81ba2491691b76882d0c636", + "sha256:8b27e6a217e786c6fbe5634d8f3f11bc63e0f80f6a5890f28863d9c45aac311b" + ], + "markers": "python_version >= '3.7'", + "version": "==0.18.6" + }, + "ruamel.yaml.clib": { + "hashes": [ + "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b", + "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4", + "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef", + "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5", + "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632", + "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6", + "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680", + "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf", + "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da", + "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6", + "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a", + "sha256:5a0e060aace4c24dcaf71023bbd7d42674e3b230f7e7b97317baf1e953e5b519", + "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6", + "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", + "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd", + "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2", + "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52", + "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd", + "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d", + "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c", + "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6", + "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb", + "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969", + "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28", + "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e", + "sha256:bc5f1e1c28e966d61d2519f2a3d451ba989f9ea0f2307de7bc45baa526de9e45", + "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4", + "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12", + "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31", + "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642", + "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e", + "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285", + "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed", + "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1", + "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7", + "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3", + "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475", + "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5", + "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76", + "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987", + "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df" + ], + "markers": "python_version < '3.13' and platform_python_implementation == 'CPython'", + "version": "==0.2.12" + }, + "safety": { + "hashes": [ + "sha256:6224dcd9b20986a2b2c5e7acfdfba6bca42bb11b2783b24ed04f32317e5167ea", + "sha256:b9e74e794e82f54d11f4091c5d820c4d2d81de9f953bf0b4f33ac8bc402ae72c" + ], + "index": "mirror", + "version": "==2.3.4" + }, + "setuptools": { + "hashes": [ + "sha256:753bb6ebf1f465a1912e19ed1d41f403a79173a9acf66a42e7e6aec45c3c16ec", + "sha256:a7fcb66f68b4d9e8e66b42f9876150a3371558f98fa32222ffaa5bced76406f8" + ], + "index": "mirror", + "version": "==75.2.0" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "snowballstemmer": { + "hashes": [ + "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", + "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a" + ], + "version": "==2.2.0" + }, + "sortedcontainers": { + "hashes": [ + "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", + "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" + ], + "version": "==2.4.0" + }, + "sqlparse": { + "hashes": [ + "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", + "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e" + ], + "markers": "python_version >= '3.8'", + "version": "==0.5.1" + }, + "stack-data": { + "hashes": [ + "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", + "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695" + ], + "version": "==0.6.3" + }, + "stevedore": { + "hashes": [ + "sha256:1efd34ca08f474dad08d9b19e934a22c68bb6fe416926479ba29e5013bcc8f78", + "sha256:9a64265f4060312828151c204efbe9b7a9852a0d9228756344dbc7e4023e375a" + ], + "markers": "python_version >= '3.8'", + "version": "==5.3.0" + }, + "tabulate": { + "hashes": [ + "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", + "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f" + ], + "markers": "python_version >= '3.7'", + "version": "==0.9.0" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.2" + }, + "tomli": { + "hashes": [ + "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", + "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed" + ], + "markers": "python_version < '3.11'", + "version": "==2.0.2" + }, + "traitlets": { + "hashes": [ + "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", + "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f" + ], + "markers": "python_version >= '3.8'", + "version": "==5.14.3" + }, + "types-pytz": { + "hashes": [ + "sha256:3e22df1336c0c6ad1d29163c8fda82736909eb977281cb823c57f8bae07118b7", + "sha256:575dc38f385a922a212bac00a7d6d2e16e141132a3c955078f4a4fd13ed6cb44" + ], + "markers": "python_version >= '3.8'", + "version": "==2024.2.0.20241003" + }, + "types-pyyaml": { + "hashes": [ + "sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570", + "sha256:d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587" + ], + "markers": "python_version >= '3.8'", + "version": "==6.0.12.20240917" + }, + "typing-extensions": { + "hashes": [ + "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", + "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" + ], + "markers": "python_version >= '3.8'", + "version": "==4.12.2" + }, + "uritemplate": { + "hashes": [ + "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0", + "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e" + ], + "markers": "python_version >= '3.6'", + "version": "==4.1.1" + }, + "urllib3": { + "hashes": [ + "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" + ], + "markers": "python_version >= '3.8'", + "version": "==2.2.3" + }, + "wcwidth": { + "hashes": [ + "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", + "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5" + ], + "version": "==0.2.13" + } + } +} diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..f60fa05 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,96 @@ +# 🖥️ Eurydice backend + +## 👪 Populate database + +In the developement environment, you can populate the database on the origin and the destination side with the following commands: + +```bash +docker exec eurydice-backend-origin-1 python manage.py populate_db +docker exec eurydice-backend-destination-1 python manage.py populate_db +``` + +You can customize the population scripts with arguments which you can list using `--help`. + +## 👮‍♀️ Code formatting + +Access the shell of the application container. + +```bash +docker compose exec backend-origin sh +``` + +Once in the container, you can access the project files. The directory of the project is by default mounted at the location `/home/eurydice`. + +The code must be formatted with [Black](https://black.readthedocs.io/en/stable/) and [isort](https://github.com/timothycrosley/isort). + +```bash +make format +``` + +To check that the code is properly formatted, run the following commands: + +```bash +make black-check +make isort-check +``` + +## 🔎 Static analysis + +[Flake8](http://flake8.pycqa.org/en/latest/) is the _linter_ integrated in the project. Run it with the command below: + +```bash +make flake8 +``` + +[MyPy](http://mypy-lang.org/) and [Pytype](https://google.github.io/pytype/) are used to check the typing of the code. + +```bash +make mypy +make pytype +``` + +[Bandit](https://bandit.readthedocs.io/en/latest/) and [Safety](https://github.com/pyupio/safety) allow to analyze respectively the security of the code and the security of the dependencies. + +```bash +make bandit +make safety +``` + +All tools to check code quality (Flake8, MyPy, Pytype, Bandit, Safety, Isort, and Black) can be launched with a single command. + +```bash +make checks +``` + +## 🧪 Testing + +Unit tests are performed with [Pytest](https://docs.pytest.org/en/latest/). +The coverage of the tests is measured with [Coverage.py](https://coverage.readthedocs.io/en/coverage-5.0.3/). + +Run the tests with the following command: + +```bash +make tests +``` + +## ⚙️ Configuration + +### 📚 Principles + +The method adopted to configure the application according to the execution environment is inspired by [The Twelve-Factor App](https://12factor.net/config) and [Two Scoops of Django 1.11: Best Practices for the Django Web Framework](https://www.roygreenfeld.com/) : + +- the preferred method for configuration is the use of environment variables. +- when the use of environment variables would make things too complex, it is possible to define configuration items in the + appropriate `eurydice/(common|origin|destination)/settings/*.py` file. + +### 🥼 Environments + +The base environment is the production environment. The test and development environments are configured from the base environment by adding configuration items. + +In development and testing, it is necessary to set the value of the environment variable `DJANGO_ENV`. + +| **Environment** | **DJANGO_ENV** | **Description** | **Settings file** | +| --------------- | -------------- | -------------------------------------------------------------------------- | ----------------- | +| Base | / | Corresponds to the production environment. | base.py | +| Development | DEV | Corresponds to the environment used by the developer locally. | dev.py | +| Test | TEST | Corresponds to the environment used to perform the tests (used by the CI). | test.py | diff --git a/backend/docker/Dockerfile b/backend/docker/Dockerfile new file mode 100644 index 0000000..49c730e --- /dev/null +++ b/backend/docker/Dockerfile @@ -0,0 +1,136 @@ +ARG EURYDICE_VERSION development + +# ------------------------------------------------------------------------------ +# Base +# ------------------------------------------------------------------------------ + +ARG DEBIAN_RELEASE="bullseye" +ARG PYTHON_BUILD_VERSION="3.10" +# packages required to build project dependencies +ARG BUILD_DEBIAN_PACKAGES="libpq-dev build-essential" +# packages required to run the project +ARG RUN_DEBIAN_PACKAGES="make postgresql-client" + +FROM python:$PYTHON_BUILD_VERSION-$DEBIAN_RELEASE AS base + +ARG BUILD_DEBIAN_PACKAGES +ARG RUN_DEBIAN_PACKAGES +ARG DEBIAN_RELEASE + +ARG DEBIAN_FRONTEND=noninteractive + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +# hadolint ignore=DL3008 +RUN apt-get update \ + && apt-get --no-install-recommends --no-install-suggests --yes --quiet install $BUILD_DEBIAN_PACKAGES $RUN_DEBIAN_PACKAGES \ + && pip install --no-cache-dir --upgrade pip==24.2 \ + && pip install --no-cache-dir pipenv==2023.9.1 \ + && apt-get clean \ + && apt-get --yes --quiet autoremove --purge \ + && rm -rf \ + /var/lib/apt/lists/* \ + /tmp/* \ + /var/tmp/* \ + /usr/share/doc/* \ + /usr/share/groff/* \ + /usr/share/info/* \ + /usr/share/linda/* \ + /usr/share/lintian/* \ + /usr/share/locale/* \ + /usr/share/man/* + +RUN mkdir /var/log/app + +WORKDIR /home/eurydice/backend + +# ------------------------------------------------------------------------------ +# Development +# ------------------------------------------------------------------------------ + +FROM base AS dev + +COPY Pipfile* ./ + +COPY Makefile . + +RUN make install-dev-docker + +COPY docker/entrypoint.sh /entrypoint.sh +COPY docker/healthcheck.py /healthcheck.py +RUN chmod +x /entrypoint.sh /healthcheck.py + + +COPY . . + +ENV PYTHONUNBUFFERED=1 + +ENTRYPOINT ["/entrypoint.sh"] +EXPOSE 8080 + +# ------------------------------------------------------------------------------ +# Compile production dependencies Stage +# ------------------------------------------------------------------------------ + +FROM base AS compile-prod + +COPY Pipfile* ./ +COPY Makefile . +RUN make install + +# ------------------------------------------------------------------------------ +# Production Image +# ------------------------------------------------------------------------------ + +FROM python:$PYTHON_BUILD_VERSION-slim-$DEBIAN_RELEASE AS prod + +ARG RUN_DEBIAN_PACKAGES + +ARG DEBIAN_FRONTEND=noninteractive + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +# hadolint ignore=DL3008 +RUN apt-get update \ + && apt-get --no-install-recommends --no-install-suggests --yes --quiet install $RUN_DEBIAN_PACKAGES \ + && apt-get clean \ + && apt-get --yes --quiet autoremove --purge \ + && rm -rf \ + /var/lib/apt/lists/* \ + /tmp/* \ + /var/tmp/* \ + /usr/share/doc/* \ + /usr/share/groff/* \ + /usr/share/info/* \ + /usr/share/linda/* \ + /usr/share/lintian/* \ + /usr/share/locale/* \ + /usr/share/man/* \ + && adduser --disabled-password --gecos "" --home /home/eurydice/backend eurydice + +RUN mkdir /var/log/app + +WORKDIR /home/eurydice/backend + +COPY --chown=eurydice:eurydice Makefile . +COPY --chown=eurydice:eurydice eurydice eurydice +COPY --chown=eurydice:eurydice --from=compile-prod /home/eurydice/backend/.venv /home/eurydice/backend/.venv +COPY --chown=eurydice:eurydice manage.py . +COPY --chown=eurydice:eurydice docker/entrypoint.sh /entrypoint.sh +COPY --chown=eurydice:eurydice docker/healthcheck.py /healthcheck.py + +# hadolint ignore=DL3044 +ENV PATH="/home/eurydice/backend/.venv/bin:$PATH" \ + PYTHONUNBUFFERED=1 + +RUN chmod +x manage.py /entrypoint.sh /healthcheck.py \ + && rm -rf ./*/tests \ + && make collectstatic + +ENTRYPOINT ["/entrypoint.sh"] +EXPOSE 8080 + +RUN chown -R eurydice:eurydice /var/log/app +USER eurydice + +ARG EURYDICE_VERSION + +ENV EURYDICE_VERSION $EURYDICE_VERSION diff --git a/backend/docker/entrypoint.sh b/backend/docker/entrypoint.sh new file mode 100644 index 0000000..cc2c37b --- /dev/null +++ b/backend/docker/entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +set -e + +if [ "x$DJANGO_MANAGEPY_MIGRATE" = 'xon' ]; then + while ! pg_isready -d "$DB_NAME" -h "$DB_HOST" -p "$DB_PORT"; do sleep 1; done + python3 manage.py migrate +fi + +exec "$@" diff --git a/backend/docker/healthcheck.py b/backend/docker/healthcheck.py new file mode 100644 index 0000000..3c68ce6 --- /dev/null +++ b/backend/docker/healthcheck.py @@ -0,0 +1,25 @@ +#!/usr/local/bin/python +import argparse +import socket + +argument_parser = argparse.ArgumentParser(description="Checks if a TCP service is up") +argument_parser.add_argument( + "-H", + "--hostname", + type=str, + default="localhost", + nargs="?", + help="Hostname of the TCP service", +) +argument_parser.add_argument( + "-p", + "--port", + type=int, + default=8080, + nargs="?", + help="Port number of the TCP service", +) +arguments = argument_parser.parse_args() + +with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect((arguments.hostname, arguments.port)) diff --git a/backend/eurydice/__init__.py b/backend/eurydice/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/common/__init__.py b/backend/eurydice/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/common/api/__init__.py b/backend/eurydice/common/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/common/api/authentication.py b/backend/eurydice/common/api/authentication.py new file mode 100644 index 0000000..3df4895 --- /dev/null +++ b/backend/eurydice/common/api/authentication.py @@ -0,0 +1,22 @@ +from django.conf import settings +from rest_framework import authentication + + +class RemoteUserAuthenticationWithCustomHeader(authentication.RemoteUserAuthentication): + """Allows DRF to authenticate with the Remote-User + HTTP header set by the reverse proxy on all requests. + + By default DRF's RemoteUserAuthentication will use the + value set in the REMOTE_USER env var. + + If we want authentication to be based on a custom HTTP + header instead, we need to prefix `header` with `HTTP_` + to tell it to look in the HTTP request headers. + + See: https://docs.djangoproject.com/en/3.2/howto/auth-remote-user/ + See: https://www.django-rest-framework.org/api-guide/authentication/#remoteuserauthentication # noqa: E501 + """ + + def __init__(self) -> None: + super().__init__() + self.header = settings.REMOTE_USER_HEADER diff --git a/backend/eurydice/common/api/docs/__init__.py b/backend/eurydice/common/api/docs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/common/api/docs/custom_spectacular.py b/backend/eurydice/common/api/docs/custom_spectacular.py new file mode 100644 index 0000000..5518232 --- /dev/null +++ b/backend/eurydice/common/api/docs/custom_spectacular.py @@ -0,0 +1,33 @@ +from typing import Any +from typing import Dict +from typing import List +from typing import Optional + +from drf_spectacular import utils + +_RedocCodeSample = Dict[str, str] +_code_samples: Dict[str, List[_RedocCodeSample]] = {} + + +def extend_schema( + operation_id: str, + code_samples: Optional[List[_RedocCodeSample]] = None, + **kwargs, +) -> Any: + """Wrapper of drf_spectacular's extend schema that supports Redoc code samples.""" + + if code_samples: + _code_samples[operation_id] = code_samples + + return utils.extend_schema(operation_id=operation_id, **kwargs) + + +def postprocessing_hook(result: dict, **_) -> dict: + """Hook that adds code samples to endpoints documentation in the OpenApi document""" + + for openapi_path in result["paths"].values(): + for openapi_operation in openapi_path.values(): + if json := _code_samples.get(openapi_operation["operationId"]): + openapi_operation["x-codeSamples"] = json + + return result diff --git a/backend/eurydice/common/api/docs/decorators.py b/backend/eurydice/common/api/docs/decorators.py new file mode 100644 index 0000000..beec828 --- /dev/null +++ b/backend/eurydice/common/api/docs/decorators.py @@ -0,0 +1,127 @@ +from django.conf import settings +from django.utils.translation import gettext_lazy as _ +from drf_spectacular import utils as spectacular_utils +from rest_framework import status + +from eurydice.common.api import serializers +from eurydice.common.api.docs import custom_spectacular +from eurydice.common.api.docs import utils as docs + +login = spectacular_utils.extend_schema_view( + get=custom_spectacular.extend_schema( + operation_id="login", + summary=_("Retrieve session cookie"), + description=_((settings.COMMON_DOCS_PATH / "user-login.md").read_text()), + parameters=[ + spectacular_utils.OpenApiParameter( + name="set-cookie", + location="header", + description=_( + "Cookie containing the CSRF token for preventing CSRF attacks." + ), + response=[status.HTTP_204_NO_CONTENT], + examples=[ + spectacular_utils.OpenApiExample( + name=settings.CSRF_COOKIE_NAME, + value=( + f"{settings.CSRF_COOKIE_NAME}=wqTG4YZkCnXUqb7E4um1kV6xCSEsKSI1mpTfeWWrej7tI2DMe0qMpU8PyvsDSva3; " # noqa: E501 + f"expires=Wed, 22 Feb 2023 17:38:37 GMT; " + f"Max-Age={settings.CSRF_COOKIE_AGE}; " + f"Path={settings.CSRF_COOKIE_PATH}; " + f"SameSite={settings.CSRF_COOKIE_SAMESITE}" + ), + ) + ], + ), + spectacular_utils.OpenApiParameter( + name="Set-Cookie", + location="header", + description=_( + "Cookie containing the session token " + "for authenticating requests from the frontend." + ), + response=[status.HTTP_204_NO_CONTENT], + examples=[ + spectacular_utils.OpenApiExample( + name=settings.SESSION_COOKIE_NAME, + value=( + f"{settings.SESSION_COOKIE_NAME}=67pb2vcxfgr3tyo33ascxhrxk21km51y; " # noqa: E501 + f"expires=Wed, 09 Mar 2022 17:38:37 GMT; " + f"HttpOnly; " + f"Max-Age={settings.SESSION_COOKIE_AGE}; " + f"Path={settings.SESSION_COOKIE_PATH}; " + f"SameSite={settings.SESSION_COOKIE_SAMESITE}" + ), + ) + ], + ), + ], + responses={ + status.HTTP_204_NO_CONTENT: spectacular_utils.OpenApiResponse( + description=_("The session cookie has been set."), + ), + # DRF returns an HTTP 403 error claiming credentials not found + # when IsAuthenticated permission is used + # see: https://github.com/encode/django-rest-framework/issues/5968 + status.HTTP_403_FORBIDDEN: docs.NotAuthenticatedResponse, + }, + code_samples=[ + { + "lang": "bash", + "label": "cURL", + "source": ( + settings.DOCS_PATH / "cookie-auth.sh" # type: ignore + ).read_text(), + } + ], + tags=[_("Account management")], + ) +) + +user_details = spectacular_utils.extend_schema_view( + get=custom_spectacular.extend_schema( + operation_id="user-me", + summary=_("Get user details"), + description=_((settings.COMMON_DOCS_PATH / "user-me.md").read_text()), + responses={ + status.HTTP_200_OK: spectacular_utils.OpenApiResponse( + description=_("User is successfully authenticated."), + response=serializers.UserSerializer, + examples=[ + spectacular_utils.OpenApiExample( + _("Get user details"), + value={ + "username": "johndoe1", + }, + ), + ], + ), + status.HTTP_401_UNAUTHORIZED: docs.NotAuthenticatedResponse, + }, + code_samples=[ + { + "lang": "bash", + "label": "cURL", + "source": ( + settings.DOCS_PATH / "user-me.sh" # type: ignore + ).read_text(), + } + ], + tags=[_("Account management")], + ) +) + +server_metadata = spectacular_utils.extend_schema_view( + get=custom_spectacular.extend_schema( + operation_id="metadata", + summary=_("Get UI metadata"), + tags=[_("Metadata")], + ) +) + + +__all__ = ( + "login", + "user_details", + "server_metadata", +) diff --git a/backend/eurydice/common/api/docs/serializers.py b/backend/eurydice/common/api/docs/serializers.py new file mode 100644 index 0000000..47083e4 --- /dev/null +++ b/backend/eurydice/common/api/docs/serializers.py @@ -0,0 +1,5 @@ +from rest_framework import serializers as drf_serializers + + +class ContactSerializer(drf_serializers.Serializer): + contact = drf_serializers.CharField() diff --git a/backend/eurydice/common/api/docs/static/account-management.md b/backend/eurydice/common/api/docs/static/account-management.md new file mode 100644 index 0000000..cc0645a --- /dev/null +++ b/backend/eurydice/common/api/docs/static/account-management.md @@ -0,0 +1,3 @@ +In Eurydice, a _physical_ user has two _logical_ user accounts: one for the origin API, and one for the destination API (on the other side of the network diode). +For Eurydice to work properly, destination user accounts are linked to origin user accounts. +This section describes user account management API endpoints. diff --git a/backend/eurydice/common/api/docs/static/openapi-retrieve.md b/backend/eurydice/common/api/docs/static/openapi-retrieve.md new file mode 100644 index 0000000..326d704 --- /dev/null +++ b/backend/eurydice/common/api/docs/static/openapi-retrieve.md @@ -0,0 +1,5 @@ +This request will give you the OpenApi3 schema for this API. +Format can be selected via content negotiation: + +- YAML: `application/vnd.oai.openapi` +- JSON: `application/vnd.oai.openapi+json` diff --git a/backend/eurydice/common/api/docs/static/openapi.md b/backend/eurydice/common/api/docs/static/openapi.md new file mode 100644 index 0000000..cfb3ca6 --- /dev/null +++ b/backend/eurydice/common/api/docs/static/openapi.md @@ -0,0 +1,3 @@ +Every operation that can be made through Eurydice's API is documented in the OpenApi3 format. + +This endpoint allows you to programmatically get the document; you can also download it by clicking the **Download** button at the top of this page. diff --git a/backend/eurydice/common/api/docs/static/transferring-files.md b/backend/eurydice/common/api/docs/static/transferring-files.md new file mode 100644 index 0000000..043f6eb --- /dev/null +++ b/backend/eurydice/common/api/docs/static/transferring-files.md @@ -0,0 +1,23 @@ +This section lists all transfer-related requests. In Eurydice, files that are meant to be transferred through the diode are called **Transferables**. + +On the origin API, you can list, check, and create _outgoing transferables_, whereas on the destination API, you deal with _incoming transferables_. +Keep in mind, these are just files before and after their _transfer_ through the diode. + +A file transfer usually goes like this: + +- On the **origin-side** API : + + - you can check if you have transfers still in progress with `GET /api/v1/transferables/`; + - you send the transferable through `POST /api/v1/transferables/`; + - you follow the progress of your transfer with `GET /api/v1/transferables/{id}/`. + +- On the **destination-side** API : + + - you list transferables recently received and check their status with `GET /api/v1/transferables/`; + - if you already knew the ID of your transferable, you could have used `GET /api/v1/transferables/{id}/`; + - you retrieve the transferable with `GET /api/v1/transferables/{id}/download/`; + - you delete the transferable with `DELETE /api/v1/transferables/{id}/`. + +**Deleting transferables after retrieving them is recommended**, especially if they are large (> 100MB): it saves up space for other users as well as for your future transfers. +Please note that Eurydice does not allow for long-term file storage. Transferred files are automatically deleted after a period of time. +Nonetheless, manual deletion is recommended to prevent congestion. diff --git a/backend/eurydice/common/api/docs/static/user-login.md b/backend/eurydice/common/api/docs/static/user-login.md new file mode 100644 index 0000000..6a77d5c --- /dev/null +++ b/backend/eurydice/common/api/docs/static/user-login.md @@ -0,0 +1,2 @@ +Accessing the Eurydice Web UI requires using a session cookie which you can retrieve using this endpoint. +The cookie will be set through the standard [`Set-Cookie`](https://developer.mozilla.org/fr/docs/Web/HTTP/Headers/Set-Cookie) header. diff --git a/backend/eurydice/common/api/docs/static/user-me.md b/backend/eurydice/common/api/docs/static/user-me.md new file mode 100644 index 0000000..d6983b9 --- /dev/null +++ b/backend/eurydice/common/api/docs/static/user-me.md @@ -0,0 +1,3 @@ +This endpoint returns the current user's username. + +The goal of this endpoint is to provide a quick and easy way to verify an authentication token, and check which user it belongs to. diff --git a/backend/eurydice/common/api/docs/static/welcome.md b/backend/eurydice/common/api/docs/static/welcome.md new file mode 100644 index 0000000..bced692 --- /dev/null +++ b/backend/eurydice/common/api/docs/static/welcome.md @@ -0,0 +1,47 @@ +# Disclaimer: this is an early access Alpha version of the software + +This is version `{EURYDICE_VERSION}`, if you find a bug please report it to {EURYDICE_CONTACT}. + +# Introduction + +## 👋 Welcome to Eurydice, learn here how to transfer your files through a network diode. + +Eurydice is a RESTful API that allows you to submit files and transfer them through a diode. +With Eurydice, you can send files from one end, retrieve them from the other, and track the progress of your transfers all along. + +## 🏁 New here? Here is how to setup your account. + +Eurydice consists of two RESTful APIs (one for each side of the diode). +Each physical user possesses two logical user accounts: one on the origin-side API, and one on the destination-side API. +If you do not have the credentials of both of your user accounts, please ask your system administrators to create them for you. + +Once you can access both APIs with their respective account, you will need to link those two accounts. +Indeed, they are initially distinct and linking them will allow you to see on the destination the files you sent from the origin. + +Here is how you should proceed: + +- First, on the origin-side API (where you send files), you need to generate an association token. + This is done with the endpoint `GET /api/v1/user/association/`, which is documented in the origin documentation (you can click _Account management_ and then _Link two user accounts_ on the left-hand side of the page to jump to it). +- On the destination-side API (where you retrieve files), you need to add this association token (by manually typing it). + This is done with the endpoint `POST /api/v1/user/association/`, which is documented in the destination documentation (you can click _Account management_ and then _Link two user accounts_ on the left-hand side of the page to jump to it). + +Congratulations! You're now able to transfer files using Eurydice! + +## ⚙️ How to use code snippets from this documentation? + +The right-side of this documentation contains some code snippets to use the API from the command line. + +For these code snippets to work, you need to setup two environment variables beforehand: + +```bash +export EURYDICE_{EURYDICE_API}_HOST={EURYDICE_HOST} + export EURYDICE_{EURYDICE_API}_AUTHTOKEN=yourAuthenticationTokenHere +``` + +Please note the space before the second export if you do not want your API authentication token to be saved in your shell history. + +## 💽 Versioning + +This instance is running Eurydice version `{EURYDICE_VERSION}`, you can view [the full changelog here](https://github.com/ANSSI-FR/eurydice/releases). + +Please note that the API specification is versioned differently from the application itself (the specification's version number is at the top of this page). diff --git a/backend/eurydice/common/api/docs/utils.py b/backend/eurydice/common/api/docs/utils.py new file mode 100644 index 0000000..8a92c93 --- /dev/null +++ b/backend/eurydice/common/api/docs/utils.py @@ -0,0 +1,58 @@ +from typing import Optional +from typing import Type + +from drf_spectacular import utils +from rest_framework import exceptions as drf_exceptions +from rest_framework import serializers + + +def _create_api_exception_serializer( + exception: Type[drf_exceptions.APIException], +) -> serializers.Serializer: + """Given a DRF APIException, return a DRF serializer for that exception. + + Args: + exception: exception to create DRF serializer. + + Returns: + DRF serializer for the given exception. + + """ + return utils.inline_serializer( + exception.__name__, + fields={"detail": serializers.CharField(default=exception.default_detail)}, + ) + + +def create_open_api_response( + exception: Type[drf_exceptions.APIException], description: Optional[str] = None +) -> utils.OpenApiResponse: + """Create an OpenApiResponse from a DRF exception. + + Args: + exception: exception to create the OpenApiResponse from. + description: optional response description. + + Returns: + OpenApiResponse for the given exception. + + """ + return utils.OpenApiResponse( + description=description or exception.default_detail, + response=_create_api_exception_serializer(exception), + ) + + +NotFoundResponse = create_open_api_response(drf_exceptions.NotFound) + +NotAuthenticatedResponse = create_open_api_response(drf_exceptions.NotAuthenticated) + +ValidationErrorResponse = create_open_api_response(drf_exceptions.ValidationError) + + +__all__ = ( + "create_open_api_response", + "NotFoundResponse", + "NotAuthenticatedResponse", + "ValidationErrorResponse", +) diff --git a/backend/eurydice/common/api/filters.py b/backend/eurydice/common/api/filters.py new file mode 100644 index 0000000..e4a2ec3 --- /dev/null +++ b/backend/eurydice/common/api/filters.py @@ -0,0 +1,19 @@ +from django.db.models.query import QuerySet +from django_filters import rest_framework as filters + +from eurydice.common.api.serializers import BytesAsHexadecimalField + + +def _filter_queryset_by_sha1(queryset: QuerySet, name: str, value: str) -> QuerySet: + """Take an hexadecimal SHA1, parse it and use it to filter the given queryset.""" + + return queryset.filter(**{name: BytesAsHexadecimalField().to_internal_value(value)}) + + +class SHA1Filter(filters.CharFilter): + """ + A filter that parses SHA1 hashes before searching the queryset. + """ + + def __init__(self, field_name: str) -> None: + super().__init__(field_name=field_name, method=_filter_queryset_by_sha1) diff --git a/backend/eurydice/common/api/middlewares.py b/backend/eurydice/common/api/middlewares.py new file mode 100644 index 0000000..47fb9d0 --- /dev/null +++ b/backend/eurydice/common/api/middlewares.py @@ -0,0 +1,101 @@ +from typing import Callable +from typing import cast + +from django import http +from django.conf import settings +from django.contrib.auth import middleware +from django.http.request import HttpRequest +from django.http.response import HttpResponse +from django.utils import timezone + +from eurydice.common.models.user import AbstractUser + + +class PersistentRemoteUserMiddlewareWithCustomHeader( + middleware.PersistentRemoteUserMiddleware +): + """Allows DRF to authenticate with the Remote-User + HTTP header set by the reverse proxy on all requests. + + By default DRF's RemoteUserAuthentication will use the + value set in the REMOTE_USER env var. + + If we want authentication to be based on a custom HTTP + header instead, we need to prefix `header` with `HTTP_` + to tell it to look in the HTTP request headers. + + See: https://docs.djangoproject.com/en/3.2/howto/auth-remote-user/ + See: https://www.django-rest-framework.org/api-guide/authentication/#remoteuserauthentication # noqa: E501 + """ + + header = settings.REMOTE_USER_HEADER + + +_GetResponseCallable = Callable[[HttpRequest], HttpResponse] + + +class AuthenticatedUserHeaderMiddleware: + """Middleware for adding authenticated user's username to response headers""" + + def __init__(self, get_response: _GetResponseCallable) -> None: # noqa: D101 + """Initialize the middleware with the function calling its successor in the pipeline + + Args: + get_response: callable used to retrieve response + """ + self.get_response = get_response + + def __call__(self, request: http.HttpRequest) -> http.HttpResponse: # noqa: D102 + """Intercept responses and inject a header before returning them + + Args: + request: from which to extract response + + Returns: + the response itself + """ + response = self.get_response(request) + if request.user.is_authenticated: + response.headers["Authenticated-User"] = request.user.username + + return response + + +class LastAccessMiddleware: + """Middleware for updating authenticated user's last_access timestamp.""" + + def __init__(self, get_response: _GetResponseCallable) -> None: + """Initialize the middleware with the function calling its successor in the pipeline + + Args: + get_response: callable used to retrieve response + """ + self.get_response = get_response + + def __call__(self, request: http.HttpRequest) -> http.HttpResponse: + """Intercept requests to update the authenticated user's last_access timestamp. + + Unlike last_login, this field gets updated every time the user accesses the + API, even when they authenticate with an API token. + + Args: + request: from which to extract response + + Returns: + the response itself + """ + response = self.get_response(request) + + if request.user.is_authenticated: + user = cast(AbstractUser, request.user) + user.last_access = timezone.now() + user.save(update_fields=["last_access"]) + + return response + + +__all__ = ( + "PersistentRemoteUserMiddlewareWithCustomHeader", + "AuthenticatedUserHeaderMiddleware", + "LastAccessMiddleware", +) diff --git a/backend/eurydice/common/api/pagination/__init__.py b/backend/eurydice/common/api/pagination/__init__.py new file mode 100644 index 0000000..7786393 --- /dev/null +++ b/backend/eurydice/common/api/pagination/__init__.py @@ -0,0 +1,3 @@ +from .openapi import EurydiceSessionPagination + +__all__ = ["EurydiceSessionPagination"] diff --git a/backend/eurydice/common/api/pagination/core.py b/backend/eurydice/common/api/pagination/core.py new file mode 100644 index 0000000..1b9e54f --- /dev/null +++ b/backend/eurydice/common/api/pagination/core.py @@ -0,0 +1,400 @@ +import json +from copy import copy +from datetime import datetime +from hashlib import md5 +from typing import Dict +from typing import List +from typing import Optional +from typing import Tuple +from typing import Union + +from django.conf import settings +from django.db.models import Model +from django.db.models import QuerySet +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ +from rest_framework import pagination +from rest_framework.exceptions import APIException +from rest_framework.exceptions import NotFound +from rest_framework.exceptions import ValidationError +from rest_framework.pagination import CursorPagination +from rest_framework.request import Request +from rest_framework.views import APIView + +from eurydice.common.api.pagination.dataclasses import Link +from eurydice.common.api.pagination.dataclasses import PageIdentifier +from eurydice.common.api.pagination.dataclasses import Query +from eurydice.common.api.pagination.dataclasses import Session + + +class PageGone(APIException): + """Exception raised when the DBTrimmer removed a page that was being explored.""" + + status_code = 410 + default_detail = "The requested page does not exist anymore" + default_code = "page_gone" + + +class EurydiceSessionPaginationWithoutOpenapi(CursorPagination): + """ + Custom pagination class, optimized for performances, without OpenAPI definitions. + + This pagination class provides the following functionalities: + - previous/current/next page identifiers: they can be used to navigate through the + database consistently (pages will always contain the same items refreshed every + query, independently from new items in the database); + - a new items detection mechanism: a boolean will indicate if new items are + available in the database; + - a count feature: the total amount of items in the database will be displayed in + requests. + """ + + no_page_size_message: str = _("Missing page size query parameter") + params_message: str = _("Query parameters that started the pagination were altered") + invalid_page_identifier_message: str = _("Invalid page identifier") + invalid_page_message: str = _("Query parameters resulted in an invalid page number") + page_or_from_message: str = _("Either page or from must be present (and not both)") + invalid_delta_message: str = _("Invalid page delta") + + from_query_param: str = "from" + from_query_description: str = _("Page identifier from which to apply a delta.") + + page_query_param: str = "page" + page_query_description: str = _("Page identifier within the paginated result set.") + + delta_query_param: str = "delta" + delta_query_description: str = _("Delta relative to the given page.") + + max_page_size: int = settings.MAX_PAGE_SIZE + page_size_query_param: str = "page_size" + + ordering: Union[str, List[str], Tuple[str, ...]] = ("-created_at", "id") + + def paginate_queryset( + self, queryset: QuerySet, request: Request, view: Optional[APIView] = None + ) -> Optional[List]: + """Gather the information needed to build the API response.""" + + self.query = self.parse_query_params(request) + + # Build the `new_items` field + new_first_item = self.fetch_leading_item_position(queryset, reverse=False) + self.first_item = None + if self.query.session and self.query.session.first_item: + self.first_item = self.query.session.first_item + self.new_items = self.query.session.first_item != new_first_item + else: + self.first_item = new_first_item + self.new_items = False + + # Set the `paginated_at` field if needed + if self.query.session: + self.paginated_at = self.query.session.paginated_at + else: + self.paginated_at = timezone.now() + + # "Freeze" the queryset if applicable + if self.query.session and self.new_items: + queryset = queryset.filter( + **{ + self.ordering[0].lstrip("-") + + "__lte": self.query.session.first_item + } + ) + + # Check if a database trimming happened + self.new_last_item = self.fetch_leading_item_position(queryset, reverse=True) + if self.query.session: + self.deleted_items = self.query.session.last_item != self.new_last_item + else: + self.deleted_items = False + + # Build the `count` field (update it if there was a trimming) + if self.query.session is None or self.deleted_items: + self.items_count = queryset.values("id").count() + else: + self.items_count = self.query.session.items_count + + # Build the `results` field + super().paginate_queryset(queryset, request, view=view) + + # Check if the page is empty and build the `pages` field + if self.page: + self.page_identifiers = self.build_page_identifiers(self.build_session()) + else: + self.handle_empty_page(request) + self.page_identifiers = self.build_page_identifiers(None) + + # Django REST Framework expects the page to be returned here + return self.page + + def parse_navigation_params( + self, request: Request + ) -> Optional[Tuple[Session, int]]: + """Load the `page`, `from` and `delta` query parameters.""" + + page_given = self.page_query_param in request.query_params + from_given = self.from_query_param in request.query_params + delta_given = self.delta_query_param in request.query_params + + if not page_given and not from_given and not delta_given: + return None + + if from_given == page_given: + raise ValidationError(self.page_or_from_message) + + try: + delta = int(request.query_params[self.delta_query_param]) + except ValueError: + raise ValidationError(self.invalid_delta_message) + except KeyError: + delta = 0 + + page_param = self.page_query_param if page_given else self.from_query_param + + try: + page_identifier = PageIdentifier.unpack(request.query_params[page_param]) + except (TypeError, ValueError): + raise ValidationError(self.invalid_page_identifier_message) + + return page_identifier.session, delta + page_identifier.offset + + def parse_query_params(self, request: Request) -> Query: + """Parse all query parameters and accordingly build a Query object.""" + + session_and_delta = self.parse_navigation_params(request) + if session_and_delta: + session, delta = session_and_delta + + self.query_params_hash = self.compute_query_params_hash(request) + if session_and_delta and self.query_params_hash != session.query_params_hash: + raise ValidationError(self.params_message) + + queried_page = (session.page_number + delta) if session_and_delta else 1 + if queried_page < 1: + raise ValidationError(self.invalid_page_message) + + return Query( + queried_page=queried_page, session=session if session_and_delta else None + ) + + def get_optimized_link(self) -> Link: + """ + Get a Link pointing to the current page. + + This method could simply always return `self.link`, but in some cases + `self.link` has a position set to None, meaning the current page was accessed + in a LIMIT/OFFSET fashion (see Links' docstring). + + If it's the case, we do not want to keep this link (too slow for the + database), so we rewrite it as a *reversed* link, with a position equal to + the database row *just after* the current page. + + Note that this optimization is only possible if the last item of the current + page and the first item of the next one have different positions. This should + always be the case, but the edge case where it's not is handled regardless. + """ + + if ( + self.link.position is None + and self.page + and self.has_next + and self.next_position + != self._get_position_from_instance(self.page[-1], self.ordering) + ): + return Link(offset=0, reverse=True, position=self.next_position) # type: ignore # noqa: E501 + + return self.link + + def build_link_for_query(self) -> Link: + """ + Build a Link that will be used by the parent to query the current page. + + This method uses the links embedded in the session query parameter to build + fast links to nearby pages. + + If there isn't any session query parameter, or if a database trimming happened + (invalidating reverse cursor-style links), this method falls back to classic + OFFSET/LIMIT pagination. + """ + + if self.page_size is None: + raise ValidationError(self.no_page_size_message) # pragma: no cover + + # Handle queries without session (basic OFFSET/LIMIT) + offset_limit_link = Link((self.query.queried_page - 1) * self.page_size) + if self.query.session is None: + return offset_limit_link + + # Calculate the queried page delta from the session's previous page + delta = self.query.queried_page - self.query.session.page_number + + # If requested, retrieve a previous page using the session's previous_link + if delta < 0: + if self.query.session.previous_link is None or self.deleted_items: + return offset_limit_link + + return self.query.session.previous_link.with_offset( + (-delta - 1) * self.page_size + ) + + # If requested, retrieve a next page using the session's next_link + if delta > 0: + if self.query.session.next_link is None: + return offset_limit_link + + return self.query.session.next_link.with_offset( + (delta - 1) * self.page_size + ) + + # If requested, retrieve the current page using the session's current_link + if self.deleted_items: + return offset_limit_link + + return self.query.session.current_link + + def build_session(self) -> Session: + """ + Build the `session` field that will be returned in the API response. + + A session consists of eight elements that will be used by future queries: + - previous, current and next links: used to quickly access nearby pages + - the current page number: used to interpret page numbers relative to the + aforementioned links + - the items count: used to know how many items were in the database when the + session began + - first and last items: used to know if new items were added to the database + since the session began, or if items were trimmed + - a hash of query parameters : to avoid accidental changes to these params + - the moment when the session was created: for informative purposes + """ + + return Session( + previous_link=self.get_previous_link(), # type: ignore + current_link=self.get_optimized_link(), + next_link=self.get_next_link(), # type: ignore + page_number=self.query.queried_page, + items_count=self.items_count, + first_item=self.first_item, + last_item=self.new_last_item, + query_params_hash=self.query_params_hash, + paginated_at=self.paginated_at, + ) + + def build_page_identifiers( + self, session: Optional[Session] + ) -> Dict[str, Optional[str]]: + """Create from 0 to 3 navigation links, to be included in the API response.""" + + if session is None or self.page_size is None: + return {"previous": None, "current": None, "next": None} + + pages = { + "previous": -1, + "current": 0, + "next": 1, + } + + result: Dict[str, Optional[str]] = {} + for page_name, offset in pages.items(): + effective_offset = (session.page_number + offset - 1) * self.page_size + if effective_offset < 0 or effective_offset >= session.items_count: + result[page_name] = None + else: + result[page_name] = PageIdentifier(session, offset).pack() + + return result + + def handle_empty_page(self, request: Request) -> None: + """Handle the fact that query parameters led to an empty page.""" + + page_given = self.page_query_param in request.query_params + from_given = self.from_query_param in request.query_params + delta_given = self.delta_query_param in request.query_params + + if not page_given and not from_given and not delta_given: + return + + if delta_given: + raise NotFound("The requested page does not exist") + + raise PageGone("The requested page was too old and was trimmed") + + def compute_query_params_hash(self, request: Request) -> bytes: + """Hash query params to ensure they stay the same across session pages.""" + + query_params = copy(request.query_params) + + query_params.pop(self.page_query_param, None) + query_params.pop(self.delta_query_param, None) + query_params.pop(self.from_query_param, None) + + query_params_json = json.dumps(dict(query_params.lists()), sort_keys=True) + + return md5(query_params_json.encode("utf-8")).digest()[:4] # nosec + + def fetch_leading_item_position( + self, queryset: QuerySet, reverse: bool + ) -> Optional[datetime]: + """Perform a query to find the position of either the first or the last item.""" + + ordering = self.ordering + if reverse: + ordering = pagination._reverse_ordering(ordering) + + latest_item = queryset.order_by(*ordering).first() + + if latest_item: + return self._get_position_from_instance(latest_item, ordering) + + return None + + def _get_position_from_instance( # type: ignore + self, + instance: Model, + ordering: Union[str, List[str], Tuple[str, ...]], + ) -> datetime: + """Override parent's position retrieval to get dates instead of strings.""" + + return getattr(instance, ordering[0].lstrip("-")) + + def get_html_context(self) -> "pagination.HtmlContext": # pragma: no cover + """Build previous and next URLs.""" + + raise NotImplementedError + + def encode_cursor(self, cursor: Link) -> Link: # type: ignore + """ + Short-circuited. + + This method was used in the parent CursorPagination implementation to embed + the given cursor inside a full URL. This behavior isn't suitable anymore, + because we now embed cursors inside the session part of the API response. + """ + + return cursor + + def decode_cursor(self, _: Request) -> Link: # type: ignore + """ + Short-circuited. + + This method was used in the parent CursorPagination implementation to extract + the given cursor from a full URL. This behavior isn't suitable anymore, + because we now extract cursors from the session query parameter. + """ + + return self.build_link_for_query() + + @property + def link(self) -> Link: + """ + Alias for the `self.cursor` attribute. + + In this implementation, several terms were adapted and renamed for coherence + purposes: + - what users call a "cursor" is a "session" in this implementation; + - what the parent implementation calls a "cursor" is a "link" in this + implementation. + """ + + return Link(*self.cursor) # type: ignore diff --git a/backend/eurydice/common/api/pagination/dataclasses.py b/backend/eurydice/common/api/pagination/dataclasses.py new file mode 100644 index 0000000..f1bc94d --- /dev/null +++ b/backend/eurydice/common/api/pagination/dataclasses.py @@ -0,0 +1,181 @@ +from base64 import urlsafe_b64decode +from base64 import urlsafe_b64encode +from datetime import datetime +from typing import NamedTuple +from typing import Optional + +from django.utils import timezone +from msgpack import packb +from msgpack import unpackb + +MICROSECONDS = 1_000_000 + + +def pack_datetime(date: Optional[datetime]) -> Optional[int]: + """Convert a datetime string to a more memory-efficient integer.""" + return int(date.timestamp() * MICROSECONDS) if date else None + + +def unpack_datetime(packed_date: Optional[int]) -> Optional[datetime]: + """Convert a memory-efficient integer to a datetime string.""" + if packed_date is None: + return None + + return timezone.make_aware(datetime.fromtimestamp(packed_date / MICROSECONDS)) + + +class Link(NamedTuple): + """ + A Link points to a page in the database. + + There are mainly three types of links: + + - OFFSET/LIMIT links: + - `position`: None + - `reverse`: False + - `offset`: the amount of items to skip to reach the beginning of the page + + - Forward links: + - `position`: a reference to the item that is just before the current page + - `reverse`: False + - `offset`: 0 + + - Reverse links: + - `position`: a reference to the item that is just after the current page + - `reverse`: True + - `offset`: 0 + + There are edge cases where `position` is not None and `offset` is not null, but + they are mostly handled in the parent class. + """ + + offset: int + reverse: bool = False + position: Optional[datetime] = None + + def with_offset(self, offset: int) -> "Link": + """Create a new Link with an offset.""" + + return Link(self.offset + offset, self.reverse, self.position) + + def packb(self) -> bytes: + """Serialize the link to bytes.""" + + topack = list(self) + topack[2] = pack_datetime(self.position) + + return packb(topack) + + @classmethod + def unpackb(cls, packed: bytes) -> "Link": + """Deserialize a link from bytes.""" + + unpacked = unpackb(packed, use_list=True) + unpacked[2] = unpack_datetime(unpacked[2]) + + return cls(*unpacked) + + +class Session(NamedTuple): + """ + A Session contains information that can be forwarded from a request to another. + + When information is transmitted through a session accross requests: + + - the performances are those of cursor-based pagination, + - and iteration over pages is consistent. + + The session token contains : + - links that will allow fast database reads, + - the page number accessed within the session, + - the amount of items in the database when the session started, + - the ID of the most recent item in the database (the "beacon"), + - and a fingerprint of side query parameters which are not supposed to change + within a session (e.g. page size and filters), + - the moment when this session was created, for informative purposes. + + These pieces of information are necessary both for performances and to enable + advanced features like pseudo-stateful navigation and new items detection. + """ + + previous_link: Optional[Link] + current_link: Link + next_link: Optional[Link] + page_number: int + items_count: int + first_item: Optional[datetime] + last_item: Optional[datetime] + query_params_hash: bytes + paginated_at: datetime + + def packb(self) -> bytes: + """Serialize the session to bytes.""" + + topack = list(self) + topack[0] = Link(*self.previous_link).packb() if self.previous_link else None + topack[1] = Link(*self.current_link).packb() + topack[2] = Link(*self.next_link).packb() if self.next_link else None + topack[5] = pack_datetime(self.first_item) + topack[6] = pack_datetime(self.last_item) + topack[8] = pack_datetime(self.paginated_at) + + return packb(topack) + + @classmethod + def unpackb(cls, packed: bytes) -> "Session": + """Deserialize a session from bytes.""" + + unpacked = unpackb(packed, use_list=True) + unpacked[0] = Link.unpackb(unpacked[0]) if unpacked[0] else None + unpacked[1] = Link.unpackb(unpacked[1]) + unpacked[2] = Link.unpackb(unpacked[2]) if unpacked[2] else None + unpacked[5] = unpack_datetime(unpacked[5]) + unpacked[6] = unpack_datetime(unpacked[6]) + unpacked[8] = unpack_datetime(unpacked[8]) + + return cls(*unpacked) + + +class PageIdentifier(NamedTuple): + """ + A PageIdentifier contains a session and an offset. + + The offset represents the identified page relative to the session's + current page number. + """ + + session: Session + offset: int + + def pack(self) -> str: + """Serialize the page identifier in a URL-safe base 64 string.""" + + return urlsafe_b64encode(packb((self.session.packb(), self.offset))).decode( + "ascii" + ) + + @classmethod + def unpack(cls, packed: str) -> "PageIdentifier": + """Deserialize a page identifier from its URL-safe base 64 format.""" + + unpacked = unpackb(urlsafe_b64decode(packed.encode("ascii")), use_list=True) + unpacked[0] = Session.unpackb(unpacked[0]) + + return cls(*unpacked) + + +class Query(NamedTuple): + """ + A Query object contains structured metadata about information in query parameters. + + Such an object is built at every API request, and takes into account the following + query parameters: + + - `page`; + - `delta`; + - `from`; + - `session`. + """ + + queried_page: int + session: Optional[Session] diff --git a/backend/eurydice/common/api/pagination/openapi.py b/backend/eurydice/common/api/pagination/openapi.py new file mode 100644 index 0000000..efe85b9 --- /dev/null +++ b/backend/eurydice/common/api/pagination/openapi.py @@ -0,0 +1,113 @@ +from collections import OrderedDict +from typing import Any +from typing import Dict +from typing import List + +from django.utils.encoding import force_str +from rest_framework.response import Response +from rest_framework.views import APIView + +from eurydice.common.api.pagination.core import EurydiceSessionPaginationWithoutOpenapi + + +class EurydiceSessionPagination(EurydiceSessionPaginationWithoutOpenapi): + """Custom pagination class, optimized for performances.""" + + def get_paginated_response(self, data: List) -> Response: + """Build the API response that will be forwarded to the user.""" + + return Response( + OrderedDict( + [ + ("offset", (self.query.queried_page - 1) * (self.page_size or 1)), + ("count", self.items_count), + ("new_items", self.new_items), + ("pages", self.page_identifiers), + ("paginated_at", self.paginated_at), + ("results", data), + ] + ) + ) + + def get_paginated_response_schema(self, schema: Dict[str, Any]) -> Dict[str, Any]: + """Add the custom fields to the parent's openapi description.""" + + return { + "type": "object", + "properties": { + "offset": {"type": "integer", "nullable": False, "minimum": 0}, + "count": { + "type": "integer", + "nullable": False, + "minimum": 0, + "example": 1, + }, + "new_items": {"type": "boolean", "nullable": False, "example": False}, + "pages": { + "type": "object", + "nullable": False, + "properties": { + "previous": { + "type": "string", + "nullable": True, + "format": "byte", + }, + "current": { + "type": "string", + "nullable": True, + "format": "byte", + }, + "next": { + "type": "string", + "nullable": True, + "format": "byte", + }, + }, + }, + "paginated_at": { + "type": "string", + "nullable": False, + "format": "date-time", + }, + "results": schema, + }, + } + + def get_schema_operation_parameters(self, _: APIView) -> List[Dict[str, Any]]: + """Add the custom query parameters to the parent's openapi description.""" + + return [ + { + "name": self.page_size_query_param, + "required": False, + "in": "query", + "description": force_str(self.page_size_query_description), + "schema": {"type": "integer", "minimum": 1}, + }, + { + "name": self.page_query_param, + "required": False, + "in": "query", + "description": force_str(self.page_query_description), + "schema": {"type": "string", "format": "byte"}, + }, + { + "name": self.from_query_param, + "required": False, + "in": "query", + "description": force_str(self.from_query_description), + "schema": {"type": "string", "format": "byte"}, + }, + { + "name": self.delta_query_param, + "required": False, + "in": "query", + "description": force_str(self.delta_query_description), + "schema": {"type": "integer"}, + }, + ] + + def get_schema_fields(self, _: APIView): # noqa: ANN201 + """Add the custom query parameters to the parent's coreapi description.""" + + raise NotImplementedError diff --git a/backend/eurydice/common/api/permissions.py b/backend/eurydice/common/api/permissions.py new file mode 100644 index 0000000..abfba81 --- /dev/null +++ b/backend/eurydice/common/api/permissions.py @@ -0,0 +1,55 @@ +"""A set of permission policies.""" + +from django import http +from django.utils.translation import gettext as _ +from rest_framework import permissions +from rest_framework import request as drf_request +from rest_framework import views + +from eurydice.common import models + + +class IsTransferableOwner(permissions.IsAuthenticated): # type: ignore + """ + Object-level permission to only allow the owner of a Transferable + (OutgoingTransferable or IncomingTransferable) to access it. + + Assumes that the object has a relation with a User through a + UserProfile. + """ + + def has_object_permission( # noqa: D102 + self, + request: drf_request.Request, + view: views.APIView, + obj: models.AbstractBaseModel, + ) -> bool: + if ( + type(obj) + # Using _default_manager, see: + # https://docs.djangoproject.com/fr/2.2/topics/db/managers/#django.db.models.Model._default_manager + ._default_manager.filter( + id=obj.id, user_profile__user__id=request.user.id + ).exists() + ): + return True + + raise http.Http404 + + +class CanViewMetrics(permissions.IsAuthenticated): # type: ignore + """ + Permission to only allow administrators and explicitly authorized + users to view rolling metrics. + """ + + message = _( + "You do not have permission to view metrics, please contact an administrator " + "if you think you should." + ) + + def has_permission(self, request: drf_request.Request, view: views.APIView) -> bool: + """Returns true if and only if the current user is allowed to view metrics.""" + return super().has_permission(request, view) and request.user.has_perm( + "eurydice_common_permissions.view_rolling_metrics" + ) diff --git a/backend/eurydice/common/api/serializers/__init__.py b/backend/eurydice/common/api/serializers/__init__.py new file mode 100644 index 0000000..93c7a70 --- /dev/null +++ b/backend/eurydice/common/api/serializers/__init__.py @@ -0,0 +1,9 @@ +from .association_token import AssociationTokenSerializer +from .hex_bytes_field import BytesAsHexadecimalField +from .user_details import UserSerializer + +__all__ = ( + "AssociationTokenSerializer", + "BytesAsHexadecimalField", + "UserSerializer", +) diff --git a/backend/eurydice/common/api/serializers/association_token.py b/backend/eurydice/common/api/serializers/association_token.py new file mode 100644 index 0000000..ff0e071 --- /dev/null +++ b/backend/eurydice/common/api/serializers/association_token.py @@ -0,0 +1,56 @@ +from typing import Dict + +from rest_framework import serializers + +from eurydice.common import association +from eurydice.common import bytes2words + + +class AssociationTokenSerializer(serializers.Serializer): + """Serialize and deserialize an AssociationToken. + + The AssociationToken is used to allow end users to associate their accounts + between the origin and the destination API. An end user will call the origin API + to get an AssociationToken, and copy that token over to the destination API. + + Attributes: + token: the encoded token provided as a list of words. + expires_at: the expiration date of the token (informational only). + + """ + + token = serializers.CharField() + expires_at = serializers.DateTimeField(read_only=True) + + def to_internal_value(self, data: dict) -> association.AssociationToken: + """Deserialize data to validated AssociationToken.""" + words = super().to_internal_value(data)["token"] + + try: + return association.AssociationToken.from_bytes(bytes2words.decode(words)) + except (bytes2words.DecodingError, association.MalformedToken): + raise serializers.ValidationError({"token": "Malformed token."}) + except association.InvalidTokenDigest: + raise serializers.ValidationError( + {"token": "Invalid association token signature."} + ) + except association.ExpiredToken: + raise serializers.ValidationError( + {"token": "The association token has expired."} + ) + + def to_representation( + self, instance: association.AssociationToken + ) -> Dict[str, str]: + """Serialize the AssociationToken to a dictionary representation.""" + instance.verify_validity_time() + + return { + "token": bytes2words.encode(instance.to_bytes()), + "expires_at": self.fields["expires_at"].to_representation( + instance.expires_at + ), + } + + +__all__ = ("AssociationTokenSerializer",) diff --git a/backend/eurydice/common/api/serializers/hex_bytes_field.py b/backend/eurydice/common/api/serializers/hex_bytes_field.py new file mode 100644 index 0000000..abe5780 --- /dev/null +++ b/backend/eurydice/common/api/serializers/hex_bytes_field.py @@ -0,0 +1,33 @@ +from drf_spectacular import types +from drf_spectacular import utils +from rest_framework import serializers + + +@utils.extend_schema_field(types.OpenApiTypes.STR) +class BytesAsHexadecimalField(serializers.Field): + """Hexadecimal string serializer for binary field.""" + + def to_representation(self, value: bytes) -> str: + """ + Args: + value (bytes): bytes to convert to hexadecimal string. + + Returns: + str: hexadecimal string from binary value. + + """ + return value.hex() + + def to_internal_value(self, data: str) -> bytes: + """ + Args: + data (str): hexadecimal string to convert to binary value. + + Returns: + bytes: bytes converted from hexadecimal string. + + """ + return bytes.fromhex(data) + + +__all__ = ("BytesAsHexadecimalField",) diff --git a/backend/eurydice/common/api/serializers/user_details.py b/backend/eurydice/common/api/serializers/user_details.py new file mode 100644 index 0000000..f42153f --- /dev/null +++ b/backend/eurydice/common/api/serializers/user_details.py @@ -0,0 +1,15 @@ +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + + +class UserSerializer(serializers.Serializer): + """Serialize a User. + + Attributes: + username: the username of the User. + + """ + + username = serializers.CharField( + help_text=_("The username of the User"), + ) diff --git a/backend/eurydice/common/api/urls.py b/backend/eurydice/common/api/urls.py new file mode 100644 index 0000000..42d3a4e --- /dev/null +++ b/backend/eurydice/common/api/urls.py @@ -0,0 +1,21 @@ +from django.conf import settings +from django.urls import path + +from eurydice.common.api import views + +urlpatterns = [ + path("schema", views.OpenApiViewSet, name="api-schema"), + path("user/me/", views.UserDetailsView.as_view(), name="user-me"), + path("metadata/", views.ServerMetadataView.as_view(), name="server-metadata"), +] + +if settings.REMOTE_USER_HEADER_AUTHENTICATION_ENABLED: + urlpatterns.append( + path("user/login/", views.UserLoginView.as_view(), name="user-login") + ) + + +handler400 = "rest_framework.exceptions.bad_request" +handler500 = "rest_framework.exceptions.server_error" + +__all__ = ("urlpatterns", "handler400", "handler500") diff --git a/backend/eurydice/common/api/views/__init__.py b/backend/eurydice/common/api/views/__init__.py new file mode 100644 index 0000000..86d6872 --- /dev/null +++ b/backend/eurydice/common/api/views/__init__.py @@ -0,0 +1,6 @@ +from .openapi import OpenApiViewSet +from .server_metadata import ServerMetadataView +from .user_details import UserDetailsView +from .user_login import UserLoginView + +__all__ = ("OpenApiViewSet", "UserDetailsView", "UserLoginView", "ServerMetadataView") diff --git a/backend/eurydice/common/api/views/openapi.py b/backend/eurydice/common/api/views/openapi.py new file mode 100644 index 0000000..530ecf8 --- /dev/null +++ b/backend/eurydice/common/api/views/openapi.py @@ -0,0 +1,34 @@ +from django.conf import settings +from django.utils.translation import gettext_lazy as _ +from drf_spectacular import utils as spectacular_utils +from drf_spectacular import views +from rest_framework import permissions +from rest_framework import status + +OpenApiViewSet = spectacular_utils.extend_schema_view( + get=spectacular_utils.extend_schema( + summary=_("Retrieve the API contract"), + description=_((settings.COMMON_DOCS_PATH / "openapi-retrieve.md").read_text()), + tags=[ + _("OpenApi3 documentation"), + ], + responses={ + status.HTTP_200_OK: spectacular_utils.OpenApiResponse( + description=_( + "The OpenApi3 document was successfully generated in the " + "requested format." + ), + ), + }, + ), +)(views.SpectacularAPIView).as_view( + # Allow anyone to view all API endpoints in specification schema + serve_public=True, + # Allow anyone to view the API specification schema + permission_classes=[permissions.AllowAny], + # Allow retrieving JSON or YAML schema, default to JSON + renderer_classes=[ + views.OpenApiJsonRenderer2, # type: ignore + views.OpenApiYamlRenderer2, # type: ignore + ], +) diff --git a/backend/eurydice/common/api/views/server_metadata.py b/backend/eurydice/common/api/views/server_metadata.py new file mode 100644 index 0000000..ad77a9f --- /dev/null +++ b/backend/eurydice/common/api/views/server_metadata.py @@ -0,0 +1,30 @@ +from django.conf import settings +from rest_framework import generics +from rest_framework import permissions +from rest_framework.request import Request +from rest_framework.response import Response + +from eurydice.common.api.docs import decorators as documentation +from eurydice.common.api.docs import serializers as docs_serializers + + +@documentation.server_metadata +class ServerMetadataView(generics.GenericAPIView): + """Get configurable metadata for the UI.""" + + permission_classes = [permissions.AllowAny] + serializer_class = docs_serializers.ContactSerializer + + def get(self, request: Request) -> Response: + """Get configurable metadata for the UI, such as contact info, + banner content and banner color. + + NB: You may also find contact information at the top of the API documentation. + """ + return Response( + { + "contact": settings.EURYDICE_CONTACT_FR, + "badge_content": settings.UI_BADGE_CONTENT, + "badge_color": settings.UI_BADGE_COLOR, + } + ) diff --git a/backend/eurydice/common/api/views/user_details.py b/backend/eurydice/common/api/views/user_details.py new file mode 100644 index 0000000..cbdacb3 --- /dev/null +++ b/backend/eurydice/common/api/views/user_details.py @@ -0,0 +1,26 @@ +import logging +from typing import cast + +from rest_framework import generics +from rest_framework import permissions + +from eurydice.common import models +from eurydice.common.api import serializers +from eurydice.common.api.docs import decorators as documentation + +logger = logging.getLogger(__name__) + + +@documentation.user_details +class UserDetailsView(generics.RetrieveAPIView): + """Access details about the current authenticated user.""" + + serializer_class = serializers.UserSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_object(self) -> models.AbstractUser: + """Returns current User.""" + return cast(models.AbstractUser, self.request.user) # the user is authenticated + + +__all__ = ("UserDetailsView",) diff --git a/backend/eurydice/common/api/views/user_login.py b/backend/eurydice/common/api/views/user_login.py new file mode 100644 index 0000000..886289e --- /dev/null +++ b/backend/eurydice/common/api/views/user_login.py @@ -0,0 +1,31 @@ +from django.core import management +from django.utils import decorators as django_decorators +from rest_framework import permissions +from rest_framework import request as drf_request +from rest_framework import response as drf_response +from rest_framework import status +from rest_framework import views + +from eurydice.common.api import authentication +from eurydice.common.api import middlewares +from eurydice.common.api.docs import decorators as documentation + +remote_user_auth = django_decorators.decorator_from_middleware( + middlewares.PersistentRemoteUserMiddlewareWithCustomHeader +) + + +@documentation.login +@django_decorators.method_decorator(remote_user_auth, name="dispatch") +class UserLoginView(views.APIView): + """View responsible for setting session and CSRF cookies.""" + + permission_classes = [permissions.IsAuthenticated] + authentication_classes = [authentication.RemoteUserAuthenticationWithCustomHeader] + + def get( + self, request: drf_request.Request, *args, **kwargs + ) -> drf_response.Response: + """Sets a session cookie and clears expired sessions for all users.""" + management.call_command("clearsessions") + return drf_response.Response(status=status.HTTP_204_NO_CONTENT) diff --git a/backend/eurydice/common/association.py b/backend/eurydice/common/association.py new file mode 100644 index 0000000..d23689e --- /dev/null +++ b/backend/eurydice/common/association.py @@ -0,0 +1,187 @@ +import datetime +import hmac +import struct +import uuid +from typing import Optional + +from django.conf import settings +from django.utils import crypto +from django.utils import timezone + + +class MalformedToken(ValueError): + """The provided token bytes don't match the expected shape.""" + + +class InvalidTokenDigest(ValueError): + """The signature of the token does not match its body.""" + + +class ExpiredToken(ValueError): + """The validity period of the token is over.""" + + +class AssociationToken: + """A token representing a UserProfile by its UUID (user_profile_id), that expires at + a given datetime (expires_at), and signed with a hmac_md5 (digest). + + To generate a new AssociationToken, users should call the constructor providing + only one argument, the user_profile_id. This will let the instance set its + own expiration time. That token can then be serialized into bytes, with the + to_bytes method. + + Example: + token = AssociationToken(user_profile_id) + token_bytes = token.to_bytes() + ... + token = AssociationToken.from_bytes(token_bytes) + user_profile_id = token.user_profile_id + + Attributes: + user_profile_id: uuid of a UserProfile + expires_at: (optional) after this datetime the token will no longer be valid. + If unspecified, is set automatically. + digest: md5 of the user_profile_id + expires_at (as 4 bytes) + + """ + + _BYTE_ORDER = ">" + _UUID_FORMAT = "16s" + _TIMESTAMP_FORMAT = "I" + _HMAC_FORMAT = "16s" + _BODY_FORMAT = _BYTE_ORDER + _UUID_FORMAT + _TIMESTAMP_FORMAT + _TOKEN_FORMAT = _BODY_FORMAT + _HMAC_FORMAT + + def __init__( + self, + user_profile_id: uuid.UUID, + expires_at: Optional[datetime.datetime] = None, + ): + self.user_profile_id: uuid.UUID = user_profile_id + self.expires_at: datetime.datetime = expires_at # type: ignore + + @property + def expires_at(self) -> datetime.datetime: + """Returns the token expiration datetime.""" + return self._expires_at + + @expires_at.setter + def expires_at(self, value: Optional[datetime.datetime]) -> None: + """Sets the token expiration datetime.""" + if value is None: + value = timezone.now() + datetime.timedelta( + seconds=settings.USER_ASSOCIATION_TOKEN_EXPIRES_AFTER + ) + elif timezone.is_naive(value): + raise ValueError( + "Received naive datetime instead of timezone aware datetime" + ) + + # remove microseconds as they are lost when serializing the token to bytes + self._expires_at = value.replace(microsecond=0) + + @property + def digest(self) -> bytes: + """Returns a computed HMAC digest from this AssociationToken. + + Returns: + bytes representing the computed hmac_md5. + + """ + + body = struct.pack( + self._BODY_FORMAT, + self.user_profile_id.bytes, + int(self.expires_at.timestamp()), + ) + + return hmac.digest( + settings.USER_ASSOCIATION_TOKEN_SECRET_KEY.encode("utf-8"), + body, + "md5", + ) + + def verify_digest(self, digest: bytes) -> None: + """Checks the provided digest against a digest computed from the present token. + + Raises: + InvalidTokenDigest: if the digests don't match. + + """ + + if not crypto.constant_time_compare(digest, self.digest): + raise InvalidTokenDigest() + + def verify_validity_time(self) -> None: + """Checks that the validity period of the token is not over. + + Raises: + ExpiredToken: if the token has expired. + + """ + + if timezone.now() > self.expires_at: + raise ExpiredToken() + + def verify(self, digest: bytes) -> None: + """Checks the integrity of this AssociationToken. + + Raises: + InvalidTokenDigest: if the provided digest don't match the body of + the token. + ExpiredToken: if the token has expired. + + """ + + self.verify_digest(digest) + self.verify_validity_time() + + def to_bytes(self) -> bytes: + """Serializes an AssociationToken to bytes. + + Returns: bytes consisting of the user_profile_id, the expiration timestamp, and + the hmac_md5 packed together. + + """ + + return struct.pack( + self._TOKEN_FORMAT, + self.user_profile_id.bytes, + int(self.expires_at.timestamp()), + self.digest, + ) + + @classmethod + def from_bytes(cls, value: bytes) -> "AssociationToken": + """Deserializes an AssociationToken from bytes and verifies it. + + Args: + value: bytes to deserialize. + + Returns: + AssociationToken + + Raises: + MalformedToken: if the token cannot be deserialized. + InvalidTokenDigest: if the signature digest doesn't match the body of + the token. + ExpiredToken: if the token has expired. + + """ + + try: + uuid_bytes, timestamp, hmac_md5 = struct.unpack(cls._TOKEN_FORMAT, value) + except struct.error: + raise MalformedToken() + + user_profile_id = uuid.UUID(bytes=uuid_bytes) + expires_at = datetime.datetime.fromtimestamp( + timestamp, tz=timezone.get_current_timezone() + ) + + obj = cls(user_profile_id, expires_at) + obj.verify(hmac_md5) + return obj + + +__all__ = ("AssociationToken",) diff --git a/backend/eurydice/common/backoffice/__init__.py b/backend/eurydice/common/backoffice/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/common/backoffice/admin.py b/backend/eurydice/common/backoffice/admin.py new file mode 100644 index 0000000..e2fa104 --- /dev/null +++ b/backend/eurydice/common/backoffice/admin.py @@ -0,0 +1,68 @@ +from datetime import datetime +from typing import Any +from typing import Dict + +from django import http +from django.contrib import admin +from django.utils import dateformat +from rest_framework.authtoken import admin as token_admin + + +class TokenAdmin(token_admin.TokenAdmin): + search_fields = ( + "key__exact", + "user__username", + ) + + +class _DisableChangePermissionMixin: + def has_change_permission(self, request: http.HttpRequest, obj: Any = None) -> bool: + return False + + +class _DisableAddPermissionMixin: + def has_add_permission(self, request: http.HttpRequest, obj: Any = None) -> bool: + return False + + +class _DisableDeletePermissionMixin: + def has_delete_permission(self, request: http.HttpRequest, obj: Any = None) -> bool: + return False + + +class _OrderingMixin: + ordering = ("-created_at",) + + +class BaseModelAdmin( # type: ignore + _DisableAddPermissionMixin, _OrderingMixin, admin.ModelAdmin +): + actions = None + help_texts: Dict[str, str] = {} + + def get_form(self, *args, **kwargs) -> Any: + kwargs.update({"help_texts": self.help_texts}) + return super().get_form(*args, **kwargs) + + def get_readonly_fields(self, request: http.HttpRequest, obj: Any = None) -> Any: + if self.fields: + return self.fields + + return super().get_readonly_fields(request, obj) + + +class BaseTabularInline( # type: ignore + _DisableChangePermissionMixin, + _DisableAddPermissionMixin, + _OrderingMixin, + admin.TabularInline, +): + extra = 0 + min_num = 0 + + +def format_date(value: datetime) -> str: + return dateformat.format(value, "j F Y H:i") + + +__all__ = ("TokenAdmin", "BaseModelAdmin", "BaseTabularInline", "format_date") diff --git a/backend/eurydice/common/backoffice/urls.py b/backend/eurydice/common/backoffice/urls.py new file mode 100644 index 0000000..16c8387 --- /dev/null +++ b/backend/eurydice/common/backoffice/urls.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path("", admin.site.urls), +] + +__all__ = ("urlpatterns",) diff --git a/backend/eurydice/common/bytes2words/__init__.py b/backend/eurydice/common/bytes2words/__init__.py new file mode 100644 index 0000000..fa364a2 --- /dev/null +++ b/backend/eurydice/common/bytes2words/__init__.py @@ -0,0 +1,6 @@ +from .bytes2words import DecodingError +from .bytes2words import EncodingError +from .bytes2words import decode +from .bytes2words import encode + +__all__ = ("encode", "decode", "EncodingError", "DecodingError") diff --git a/backend/eurydice/common/bytes2words/bytes2words.py b/backend/eurydice/common/bytes2words/bytes2words.py new file mode 100644 index 0000000..b8a2f79 --- /dev/null +++ b/backend/eurydice/common/bytes2words/bytes2words.py @@ -0,0 +1,103 @@ +"""Converts bytes into human readable words (or words back into bytes) +using a fixed set of words, defined in a given plain text file. + +Two bytes = one word. + +For example from 16 bytes, 8 words will be generated like this: +"prudemment brillants silly prealables CITROEN ilknur mare SOUPCONNES" + +""" + +import pathlib +import re +from typing import Dict +from typing import List +from typing import Literal + +from django.conf import settings + +_BYTE_ORDER: Literal["big"] = "big" + +_DEFAULT_WORDLIST_PATH: pathlib.Path = ( + pathlib.Path(__file__).resolve().parent / "word_list.txt" +) +_WORDLIST_PATH: str = getattr(settings, "BYTES2WORDS_DICT", str(_DEFAULT_WORDLIST_PATH)) + +# List of words in the file +_WORDS: List[str] = pathlib.Path(_WORDLIST_PATH).read_text().split("\n") + +# From a word, retrieves the index of that word in _WORDS +_INDEX: Dict[str, int] = {word: i for i, word in enumerate(_WORDS)} + +_NON_ALPHANUMERIC = r"[^a-zA-Z]+" + +_SEPARATOR = " " + + +class EncodingError(ValueError): + """Signal an error during the encoding process.""" + + +class DecodingError(ValueError): + """Signal an error during the decoding process.""" + + +def encode(data: bytes) -> str: + """Converts bytes into a string of readable words. + Data must be of even length. + + Splits the given data into groups of two bytes. (0x12345678 -> 0x1234, 0x5678) + For each group of two bytes, + casts those two bytes into an int, (0x1234 -> 4660) + and uses that int as an index to pick a word from the + list of words defined in the constructor (self._words[4660]) + Glues all the picked words together with spaces. + + Args: + data: bytes of even length (ie. len(data) % 2 == 0) + + Returns: + String of x words. (x = len(data) / 2) + + Raises: + ValueError: If len(data) is not a multiple of 2. + + """ + if len(data) % 2 != 0: + raise EncodingError("Data length must be even.") + + return _SEPARATOR.join( + [ + _WORDS[int.from_bytes(data[i * 2 : i * 2 + 2], _BYTE_ORDER)] + for i in range(int(len(data) / 2)) + ] + ) + + +def decode(words: str) -> bytes: + """Converts words back into bytes. + + For each word, retrieves its index as an int, + converts the index into 2 bytes, and glues all those bytes together. + + Args: + words: A string of space-separated words + + Returns: + bytes + + """ + + decoded_bytes = [] + for word in re.split(_NON_ALPHANUMERIC, words): + try: + position = _INDEX[word] + except KeyError: + raise DecodingError(f"Word '{word}' cannot be found in the index.") + else: + decoded_bytes.append(position.to_bytes(2, _BYTE_ORDER)) + + return b"".join(decoded_bytes) + + +__all__ = ("encode", "decode", "EncodingError", "DecodingError") diff --git a/backend/eurydice/common/bytes2words/word_list.txt b/backend/eurydice/common/bytes2words/word_list.txt new file mode 100644 index 0000000..45aa639 --- /dev/null +++ b/backend/eurydice/common/bytes2words/word_list.txt @@ -0,0 +1,65536 @@ +ABACULE +ABAISSANT +ABAISSAT +ABAISSERAI +ABAISSERONT +ABAJOUE +ABANDONNAMES +ABANDONNEREZ +ABASOURDIMES +ABASOURDISSE +ABATAGE +ABATARDIRAI +ABATARDIRONT +ABATARDISSEZ +ABATTABLE +ABATTEE +ABATTEZ +ABATTISSIONS +ABATTRAIT +ABATTUES +ABBAYE +ABCEDA +ABCEDASSES +ABCEDERA +ABCEDERONS +ABDICATION +ABDIQUASSE +ABDIQUENT +ABDIQUERIEZ +ABDOMENS +ABDUCTRICE +ABEILLERS +ABENAQUIS +ABERRANCE +ABERRASSIEZ +ABERRERAI +ABERRERONT +ABETIE +ABETIRENT +ABETISSANT +ABETISSIEZ +ABHORRAMES +ABHORRE +ABHORRERAS +ABHORRIONS +ABIETINEE +ABIMANT +ABIMEE +ABIMERENT +ABIMONS +ABJECTE +ABJURAIT +ABJURATES +ABJURERA +ABJURERONS +ABLACTATIONS +ABLATAS +ABLATEES +ABLATEREZ +ABLATION +ABLERET +ABNEGATIONS +ABOIERAIT +ABOITEAUX +ABOLIRAIT +ABOLISSAIS +ABOLIT +ABOLITIVE +ABOMINAIS +ABOMINAT +ABOMINERAI +ABOMINERONT +ABONDAIT +ABONDANTS +ABONDEE +ABONDERAIT +ABONDIEZ +ABONNAS +ABONNEES +ABONNERAS +ABONNIE +ABONNIRAIT +ABONNISSAIS +ABONNISSIONS +ABORDAGES +ABORDASSES +ABORDERA +ABORDERONS +ABORIGENE +ABORNASSE +ABORNEMENT +ABORNERENT +ABORNONS +ABOUCHAI +ABOUCHASSIEZ +ABOUCHER +ABOUCHERIONS +ABOULAIENT +ABOULASSIONS +ABOULERAIENT +ABOULES +ABOUTA +ABOUTASSE +ABOUTEMENT +ABOUTERENT +ABOUTIES +ABOUTIRAS +ABOUTISSAIT +ABOUTISSIONS +ABOYAIT +ABOYATES +ABOYEUSES +ABRACADABRAS +ABRASASSE +ABRASENT +ABRASERIEZ +ABRASIMETRES +ABREAGIMES +ABREAGIRIEZ +ABREAGISSENT +ABREGEAI +ABREGEASSIEZ +ABREGER +ABREGERIONS +ABREUVAI +ABREUVASSIEZ +ABREUVER +ABREUVERIONS +ABREVIATEUR +ABRIASSE +ABRICOTEE +ABRIEES +ABRIEREZ +ABRIS +ABRITASSENT +ABRITER +ABRITERIONS +ABRIVENT +ABROGE +ABROGEAS +ABROGENT +ABROGEREZ +ABROUTIE +ABROUTIRENT +ABROUTISSANT +ABROUTIT +ABRUTIMES +ABRUTIRIEZ +ABSCISSE +ABSENTAIENT +ABSENTENT +ABSENTERIEZ +ABSIDAL +ABSIDIOLE +ABSOLUES +ABSOLUSSIEZ +ABSOLUTOIRE +ABSOLVIEZ +ABSORBAMES +ABSORBASSES +ABSORBERA +ABSORBERONS +ABSORBONS +ABSOUDRAIT +ABSOUTE +ABSTENIONS +ABSTENUS +ABSTIENDRONS +ABSTINENTE +ABSTINSSIONS +ABSTRACTIVE +ABSTRAIRAIS +ABSTRAIT +ABSTRAYIEZ +ABSURDITES +ABUSASSE +ABUSENT +ABUSERIEZ +ABUSIEZ +ABUTAI +ABUTASSIEZ +ABUTERAI +ABUTERONT +ABYSSALE +ACADEMISME +ACAGNARDES +ACALEPHES +ACARIENS +ACATENE +ACCABLAIENT +ACCABLASSENT +ACCABLEMENTS +ACCABLEREZ +ACCALMIE +ACCAPARANTES +ACCAPARE +ACCAPARERAIS +ACCAPAREUR +ACCASTILLES +ACCEDAMES +ACCEDASSIONS +ACCEDERAIT +ACCEDIEZ +ACCELERANDOS +ACCELERATEUR +ACCELERER +ACCENTUABLES +ACCENTUASSES +ACCENTUEL +ACCENTUERAIT +ACCENTUIEZ +ACCEPTAIS +ACCEPTASSES +ACCEPTENT +ACCEPTERIEZ +ACCEPTIEZ +ACCESSIONS +ACCIDENTAS +ACCIDENTEES +ACCIDENTEZ +ACCIDENTS +ACCLAMAI +ACCLAMASSIEZ +ACCLAME +ACCLAMERAS +ACCLAMIONS +ACCLIMATANT +ACCLIMATES +ACCOINTAMES +ACCOINTAT +ACCOINTERAIS +ACCOINTEZ +ACCOLAIENT +ACCOLASSIONS +ACCOLERA +ACCOLERONS +ACCOMBANTS +ACCOMMODANTS +ACCOMMODEREZ +ACCOMPAGNA +ACCOMPAGNAS +ACCOMPAGNE +ACCOMPAGNIEZ +ACCOMPLIS +ACCONS +ACCORAS +ACCORDABLE +ACCORDANT +ACCORDAT +ACCORDEREZ +ACCORDEZ +ACCORENT +ACCORERIEZ +ACCORNEES +ACCOSTA +ACCOSTANT +ACCOSTEE +ACCOSTERENT +ACCOSTONS +ACCOTASSE +ACCOTEMENT +ACCOTERENT +ACCOTOIR +ACCOUANT +ACCOUCHAI +ACCOUCHER +ACCOUCHIONS +ACCOUDASSE +ACCOUDEMENT +ACCOUDERENT +ACCOUDOIR +ACCOUERAIENT +ACCOUES +ACCOUPLAIS +ACCOUPLAT +ACCOUPLERAI +ACCOUPLERONT +ACCOURCI +ACCOURCIRAS +ACCOURCIT +ACCOURRA +ACCOURRONT +ACCOURUSSENT +ACCOUTRAIT +ACCOUTRATES +ACCOUTRES +ACCOUTUMAMES +ACCOUTUMAT +ACCOUTUMEZ +ACCOUVAIT +ACCOUVATES +ACCOUVERAIT +ACCOUVEURS +ACCREDITAIS +ACCREDITAT +ACCREDITERAI +ACCREDITIONS +ACCRESCENT +ACCRETANT +ACCRETEE +ACCRETERENT +ACCRETIONS +ACCROCHAIENT +ACCROCHEREZ +ACCROCHEZ +ACCROISSANT +ACCROIT +ACCROITRIONS +ACCROUPIRAI +ACCROUPIRONT +ACCROUPISSEZ +ACCRURENT +ACCUEIL +ACCUEILLERA +ACCUEILS +ACCULASSE +ACCULEMENT +ACCULERENT +ACCULONS +ACCULTURASSE +ACCULTUREE +ACCULTURONS +ACCUMULE +ACCUMULERAS +ACCUMULIONS +ACCUSAMES +ACCUSATEUR +ACCUSE +ACCUSERAS +ACCUSIONS +ACENSAIS +ACENSAT +ACENSERAIS +ACENSEZ +ACERACEES +ACERASSES +ACEREE +ACERERENT +ACERICULTEUR +ACESCENCE +ACETABULUM +ACETEUSE +ACETIFIAS +ACETIFIE +ACETIFIERAS +ACETIFIIONS +ACETOMETRE +ACETYLURES +ACHALANDAGE +ACHALANDER +ACHALANTES +ACHALAT +ACHALERAIS +ACHALEZ +ACHARDS +ACHARNAS +ACHARNEES +ACHARNERAS +ACHARNIONS +ACHEEN +ACHEMINAIT +ACHEMINATES +ACHEMINES +ACHETABLES +ACHETASSES +ACHETERA +ACHETERONS +ACHETONS +ACHEVAIS +ACHEVAT +ACHEVERAI +ACHEVERONT +ACHIGANS +ACHOPPAI +ACHOPPASSIEZ +ACHOPPER +ACHOPPERIONS +ACHROMAT +ACHROMATISAS +ACHROMATISES +ACHYLIES +ACIDIFIABLE +ACIDIFIANTS +ACIDIFIERAIT +ACIDIFIIEZ +ACIDIPHILES +ACIDULAI +ACIDULASSIEZ +ACIDULERAI +ACIDULERONT +ACIERAGE +ACIERASSENT +ACIEREES +ACIEREREZ +ACIERIE +ACINETIEN +ACLINIQUE +ACNEIQUES +ACON +ACOQUINA +ACOQUINASSES +ACOQUINENT +ACOQUINERIEZ +ACORES +ACOUMETRIES +ACQUERESSES +ACQUERRA +ACQUERRONT +ACQUIESCES +ACQUISITIF +ACQUISSIONS +ACQUITTAIT +ACQUITTATES +ACQUITTES +ACRETE +ACRIMONIE +ACRODYNIE +ACROMIALES +ACROPOLES +ACRYLIQUES +ACTANCIELLE +ACTASSIONS +ACTERAIENT +ACTES +ACTINIDES +ACTINIQUES +ACTINOMETRES +ACTIONNABLE +ACTIONNARIAL +ACTIONNER +ACTIONNISMES +ACTIVANTE +ACTIVATES +ACTIVEMENT +ACTIVEREZ +ACTIVIONS +ACTRICES +ACTUALISANT +ACTUALISERA +ACTUALITES +ACTUATIONS +ACULEATE +ACUPONCTEURS +ACYCLIQUES +ADAGIO +ADAMIQUE +ADAPTABLES +ADAPTASSES +ADAPTATIONS +ADAPTERA +ADAPTERONS +ADDAX +ADDICTION +ADDITIFS +ADDITIONNANT +ADDITIONNEES +ADDITIVEES +ADDUITS +ADENOPATHIES +ADEQUATEMENT +ADEXTREE +ADHERAS +ADHERENCES +ADHERERAIT +ADHERIEZ +ADHESIVITES +ADIAPHORESES +ADIPOLYSE +ADIRE +ADJACENTES +ADJECTIVEE +ADJECTIVERAS +ADJECTIVIONS +ADJECTIVISEZ +ADJOIGNENT +ADJOIGNISSES +ADJOINDRAIT +ADJOINTE +ADJUGEASSE +ADJUGEONS +ADJUGERIEZ +ADJURAIENT +ADJURASSIONS +ADJURERA +ADJURERONS +ADJUVANTS +ADMETTENT +ADMETTRAIT +ADMINICULES +ADMINISTRAS +ADMINISTRER +ADMIRABLES +ADMIRASSES +ADMIRATIONS +ADMIRER +ADMIRERIONS +ADMISES +ADMISSIONS +ADMONESTAIS +ADMONESTAT +ADMONESTERAI +ADNEES +ADOLESCENCES +ADONIENNE +ADONNAMES +ADONNE +ADONNERAS +ADONNIONS +ADOPTAMES +ADOPTASSIONS +ADOPTERAIENT +ADOPTES +ADOPTIFS +ADORAI +ADORASSIEZ +ADORE +ADORERAS +ADORIONS +ADORNASSENT +ADORNER +ADORNERIONS +ADOSSA +ADOSSASSES +ADOSSENT +ADOSSERIEZ +ADOUBAI +ADOUBASSIEZ +ADOUBER +ADOUBERIONS +ADOUCIES +ADOUCIREZ +ADOUCISSAIT +ADOUCISSEUR +ADRAGANTES +ADRESSA +ADRESSANT +ADRESSEE +ADRESSERENT +ADRESSONS +ADSCRIT +ADSORBANT +ADSORBAT +ADSORBERAIS +ADSORBEZ +ADULAIENT +ADULASSES +ADULATRICES +ADULERAIT +ADULESCENTE +ADULTERAIENT +ADULTERERA +ADULTERERONS +ADULTERONS +ADVENTICE +ADVENUES +ADVERSATIFS +ADVIENDRONT +ADYNAMIQUES +AEGOSOMES +AERAIT +AERATES +AERENT +AERERIEZ +AERIENNES +AEROBIC +AEROBIQUE +AEROGASTRIES +AEROGRAPHES +AEROMOBILES +AERONAUTE +AERONOMIE +AEROPHONES +AEROSONDAGE +AEROTHERME +AESCHNES +AFARS +AFFABULAIENT +AFFABULEE +AFFABULERENT +AFFABULONS +AFFADIRAIENT +AFFADIS +AFFAIBLIE +AFFAIBLIRENT +AFFAIBLISSEZ +AFFAIRAIT +AFFAIRATES +AFFAIRES +AFFAISSAI +AFFAISSER +AFFAITAGES +AFFAITASSES +AFFAITENT +AFFAITERIEZ +AFFALAI +AFFALASSIEZ +AFFALER +AFFALERIONS +AFFAMAIENT +AFFAMASSIONS +AFFAMERAIENT +AFFAMES +AFFEAGEA +AFFEAGEASSES +AFFEAGERA +AFFEAGERONS +AFFECTAI +AFFECTASSIEZ +AFFECTER +AFFECTERIONS +AFFECTIONNAI +AFFERAMES +AFFERE +AFFERERAIT +AFFERIEZ +AFFERMAIT +AFFERMATAIRE +AFFERMES +AFFERMIRAI +AFFERMIRONT +AFFERMISSES +AFFETEE +AFFICHABLE +AFFICHAS +AFFICHEES +AFFICHEREZ +AFFICHEUSE +AFFIDE +AFFILAIT +AFFILATES +AFFILERAIENT +AFFILES +AFFILIANT +AFFILIATIONS +AFFILIERAIT +AFFILIIEZ +AFFINAI +AFFINASSIEZ +AFFINER +AFFINERIES +AFFINEZ +AFFIQUETS +AFFIRMASSENT +AFFIRMATION +AFFIRMENT +AFFIRMERIEZ +AFFIXALE +AFFLEURAI +AFFLEURER +AFFLICTION +AFFLIGEAMES +AFFLIGES +AFFLOUAIT +AFFLOUATES +AFFLOUERAIT +AFFLOUIEZ +AFFLUAS +AFFLUENCES +AFFLUERAIT +AFFLUIEZ +AFFOLANT +AFFOLAT +AFFOLERAI +AFFOLERONT +AFFOUAGEAI +AFFOUAGERAI +AFFOUILLA +AFFOUILLENT +AFFOURAGEA +AFFOURAGER +AFFOURCHAIS +AFFOURCHAT +AFFOURCHEZ +AFFRANCHIE +AFFRETANT +AFFRETEE +AFFRETERAIT +AFFRETEURS +AFFRIANDAI +AFFRIANDERAI +AFFRICHAIT +AFFRICHATES +AFFRICHERAIT +AFFRICHIEZ +AFFRIOLANTE +AFFRIOLATES +AFFRIOLERAIT +AFFRIOLIEZ +AFFRONTAIT +AFFRONTATES +AFFRONTES +AFFRUITA +AFFRUITASSES +AFFRUITERA +AFFRUITERONS +AFFUBLAIS +AFFUBLAT +AFFUBLERAI +AFFUBLERONT +AFFUTAGE +AFFUTASSENT +AFFUTER +AFFUTERIONS +AFFUTIAUX +AFGHANS +AFOCALE +AFRICANISAIS +AFRICANISEZ +AFROBEAT +AGACAMES +AGACASSIONS +AGACERA +AGACERIEZ +AGADAS +AGAMIDE +AGAPETES +AGASSIN +AGATISES +AGENAISES +AGENCASSENT +AGENCEMENTS +AGENCEREZ +AGENCIERS +AGENDANT +AGENDEE +AGENDERENT +AGENDONS +AGENOUILLAT +AGENOUILLES +AGENTIF +AGGADAH +AGGLOMERAMES +AGGLOMERATES +AGGLOMERERAI +AGGLUTINAI +AGGLUTINASSE +AGGLUTINEE +AGGLUTININES +AGGRAVAMES +AGGRAVERA +AGGRAVERONS +AGHLABIDES +AGIO +AGIOTANT +AGIOTENT +AGIOTERIEZ +AGIOTIEZ +AGIRENT +AGISSAIS +AGISSES +AGITAIT +AGITATES +AGITEE +AGITERENT +AGITONS +AGNATE +AGNEAUX +AGNELANT +AGNELEE +AGNELERAIT +AGNELETS +AGNELLERA +AGNELLERONT +AGNOSTIQUE +AGONIES +AGONIREZ +AGONISAIT +AGONISASSIEZ +AGONISERAIS +AGONISEZ +AGONISSES +AGONITES +AGRAFAGE +AGRAFASSENT +AGRAFER +AGRAFERIONS +AGRAINA +AGRAINASSE +AGRAINENT +AGRAINERIEZ +AGRAIRES +AGRANDI +AGRANDIRAS +AGRANDISSAIT +AGRANDISSIEZ +AGRAPHIQUES +AGREAGES +AGREASSES +AGREENT +AGREERIEZ +AGREG +AGREGATS +AGREGEASSE +AGREGEONS +AGREGERIEZ +AGREIONS +AGREMENTASSE +AGREMENTENT +AGREONS +AGRESSASSE +AGRESSENT +AGRESSERIEZ +AGRESSIEZ +AGRESSONS +AGRICULTURE +AGRIFFASSE +AGRIFFENT +AGRIFFERIEZ +AGRILES +AGRIPPAIS +AGRIPPAT +AGRIPPERAI +AGRIPPERONT +AGROLOGIES +AGUERRIMES +AGUERRIRIEZ +AGUETS +AGUICHAIT +AGUICHASSIEZ +AGUICHERAI +AGUICHERONT +AGUILLA +AGUILLASSES +AGUILLERA +AGUILLERONS +AHANA +AHANASSES +AHANERAIENT +AHANES +AHEURTAIT +AHEURTATES +AHEURTES +AHURIRA +AHURIRONS +AHURISSE +AHURITES +AICHASSE +AICHENT +AICHERIEZ +AIDA +AIDAS +AIDEAUX +AIDERAS +AIDIONS +AIEUX +AIGLONNE +AIGRELETTE +AIGREUR +AIGRIRAI +AIGRIRONT +AIGRISSES +AIGUAGE +AIGUILLA +AIGUILLASSE +AIGUILLEES +AIGUILLEREZ +AIGUILLETAT +AIGUILLIERS +AIGUISA +AIGUISANT +AIGUISEE +AIGUISERAIT +AIGUISEURS +AIKIDO +AILERON +AILLADE +AILLASSENT +AILLER +AILLERIONS +AILLOLI +AIMAIS +AIMANTANT +AIMANTATIONS +AIMANTERAIT +AIMANTIEZ +AIMAT +AIMERAIS +AIMEZ +AINOU +AIRAINS +AIRASSIONS +AIREDALES +AIRERAIS +AIREZ +AISE +AISSETTES +AJACCIENNES +AJOINTANT +AJOINTEE +AJOINTERENT +AJOINTONS +AJOURAIT +AJOURATES +AJOURERAIT +AJOURIEZ +AJOURNANT +AJOURNEE +AJOURNERAIT +AJOURNIEZ +AJOUTAIT +AJOUTATES +AJOUTERAIT +AJOUTIEZ +AJUSTAIENT +AJUSTASSIONS +AJUSTERA +AJUSTERONS +AJUSTOIR +AKANS +AKINESIES +AKVAVITS +ALABASTRITES +ALAMBICS +ALAMBIQUAS +ALAMBIQUEES +ALAMBIQUEREZ +ALANDIER +ALANGUIRAIS +ALANGUISSIEZ +ALARMAI +ALARMASSE +ALARMENT +ALARMERIEZ +ALARMISTE +ALATERNES +ALBANOPHONE +ALBEDOS +ALBIENNE +ALBITES +ALBUGOS +ALBUMINEMIES +ALCALESCENT +ALCALIMETRES +ALCALINISAT +ALCALINISES +ALCALOIDE +ALCAPTONE +ALCENES +ALCIDES +ALCOOLE +ALCOOLIQUES +ALCOOLISAS +ALCOOLISE +ALCOOLISERAS +ALCOOLISIONS +ALCOOTESTS +ALCOYLES +ALCYONS +ALDINES +ALE +ALENCONNAIS +ALENTIR +ALENTIRIONS +ALENTISSES +ALEOUTES +ALEPPINE +ALERTAMES +ALERTE +ALERTERAIT +ALERTIEZ +ALESAIT +ALESATES +ALESERAIT +ALESEURS +ALESOIR +ALEURODES +ALEVINAI +ALEVINASSIEZ +ALEVINERAI +ALEVINERONT +ALEVINS +ALEXITHYMIE +ALFA +ALGAL +ALGEBRIQUE +ALGEROISE +ALGINATE +ALGOLOGUES +ALGONQUIEN +ALIBIS +ALICYCLIQUES +ALIENAIENT +ALIENASSENT +ALIENATION +ALIENERAI +ALIENERONT +ALIFERES +ALIGNAS +ALIGNEES +ALIGNERAS +ALIGNIONS +ALIMENTAIRE +ALIMENTE +ALIMENTERAS +ALIMENTIONS +ALIPHATIQUE +ALISMA +ALITAIENT +ALITASSIONS +ALITERA +ALITERONS +ALIZARIS +ALKEKENGES +ALLAIENT +ALLAITANTE +ALLAITATES +ALLAITES +ALLANTES +ALLAS +ALLE +ALLECHANTS +ALLECHEE +ALLECHERAIT +ALLECHIEZ +ALLEGEABLES +ALLEGEASSE +ALLEGEMENTS +ALLEGERENT +ALLEGIES +ALLEGIRAS +ALLEGISSAIT +ALLEGITES +ALLEGORISAIT +ALLEGORISE +ALLEGORISONS +ALLEGRO +ALLEGUASSE +ALLEGUENT +ALLEGUERIEZ +ALLELES +ALLEMANDS +ALLERGIDES +ALLEUTIER +ALLIAGES +ALLIANT +ALLICINES +ALLIERAIT +ALLIGATOR +ALLODIAL +ALLOGREFFE +ALLONGEAI +ALLONGER +ALLONGERIONS +ALLOPATHIE +ALLOSAURE +ALLOTIES +ALLOTIREZ +ALLOTISSE +ALLOTITES +ALLOUAMES +ALLOUCHIER +ALLOUERAIS +ALLOUEZ +ALLUMAIENT +ALLUMASSIONS +ALLUMERAIENT +ALLUMES +ALLUMEUSES +ALLURES +ALLUVIALES +ALLUVIONNEE +ALMAGESTES +ALMASILIUMS +ALMORAVIDES +ALOPECIE +ALOUCHIERS +ALOURDIS +ALOURDISSEZ +ALPAGA +ALPAGUAIT +ALPAGUATES +ALPAGUERAIT +ALPAGUIEZ +ALPHA +ALPHABETISEE +ALPINISME +ALSACIEN +ALTERABILITE +ALTERAMES +ALTERASSIONS +ALTERENT +ALTERERIEZ +ALTERNA +ALTERNANTES +ALTERNATEUR +ALTERNEES +ALTERNEREZ +ALTERONS +ALTI +ALTIMETRE +ALTISE +ALUCITES +ALUMINAIRE +ALUMINASSIEZ +ALUMINERA +ALUMINERIEZ +ALUMINIAGES +ALUMINITES +ALUMNATS +ALUNANT +ALUNEE +ALUNERENT +ALUNI +ALUNIRAI +ALUNIRONT +ALUNISSES +ALUS +ALVINE +ALYSSUMS +AMADOUA +AMADOUASSES +AMADOUENT +AMADOUERIEZ +AMADOUVIER +AMAIGRIRAI +AMAIGRIRONT +AMALGAMA +AMALGAMASSES +AMALGAMENT +AMALGAMERIEZ +AMANCHA +AMANCHASSES +AMANCHERA +AMANCHERONS +AMANDEE +AMANITES +AMAREYEUR +AMARINAGES +AMARINASSES +AMARINERA +AMARINERONS +AMARNIENS +AMARRAS +AMARREES +AMARREREZ +AMASSANT +AMASSEE +AMASSERENT +AMASSEURS +AMATEURISME +AMATIRA +AMATIRONS +AMATISSEZ +AMAUROTIQUE +AMAZONIEN +AMBASSADEURS +AMBIANCAS +AMBIANCER +AMBIANCIONS +AMBIFIA +AMBIFIASSES +AMBIFIERA +AMBIFIERONS +AMBIGUITE +AMBITIEUSE +AMBITIONNEE +AMBITIONS +AMBLAIENT +AMBLASSIONS +AMBLERAIT +AMBLEURS +AMBLYOPIES +AMBONS +AMBRASSENT +AMBREINES +AMBREREZ +AMBREZ +AMBROSIAQUE +AMBULANCE +AMBULATOIRE +AMELIORABLE +AMELIORANTS +AMELIORERA +AMELIORERONS +AMENAGEA +AMENAGEASSE +AMENAGEMENTS +AMENAGERENT +AMENAGEUSES +AMENAMES +AMENDA +AMENDASSE +AMENDEMENT +AMENDERENT +AMENDONS +AMENERAIT +AMENIEZ +AMENSALE +AMENUISAI +AMENUISER +AMERASIENNE +AMERICANISA +AMERICANISAT +AMERICIUMS +AMERLOS +AMERRIRAIENT +AMERRIS +AMERRISSEZ +AMETABOLE +AMEUBLEMENT +AMEUBLIRAIS +AMEUBLISSIEZ +AMEULONNAMES +AMEULONNE +AMEULONNERAS +AMEULONNIONS +AMEUTASSE +AMEUTEMENT +AMEUTERENT +AMEUTONS +AMIANTEE +AMIBIASES +AMICALES +AMIDON +AMIDONNAS +AMIDONNEES +AMIDONNEREZ +AMIDONNIERE +AMIENOIS +AMIMIQUE +AMINCIRAIS +AMINCISSENT +AMINEES +AMINOSIDE +AMIS +AMITIEUSES +AMMANIENS +AMMONIAC +AMMONITRATE +AMNESIQUE +AMNIOTIQUES +AMNISTIANTE +AMNISTIATES +AMNISTIERAIT +AMNISTIIEZ +AMOCHAS +AMOCHEES +AMOCHEREZ +AMODAL +AMODIANT +AMODIATES +AMODIENT +AMODIERIEZ +AMOINDRIE +AMOINDRIRENT +AMOINDRITES +AMOLLIRAIENT +AMOLLIS +AMOMES +AMONCELERENT +AMONCELLERAI +AMONCELLES +AMORALISMES +AMORCAIENT +AMORCASSIONS +AMORCERAIENT +AMORCES +AMORCONS +AMORDANCER +AMORPHES +AMORTIRAIT +AMORTISSEUR +AMOUILLA +AMOUILLASSE +AMOUILLERA +AMOUILLERONS +AMOURACHES +AMOUREUX +AMPELITE +AMPERE +AMPHIBIOSES +AMPHIGENES +AMPHIPHILE +AMPHISBENES +AMPHOLYTES +AMPLECTIVE +AMPLIATEURS +AMPLIFIAI +AMPLIFIASSE +AMPLIFIES +AMPOULE +AMPUTAIT +AMPUTATES +AMPUTERAIENT +AMPUTES +AMUIE +AMUIRENT +AMUISSANT +AMUIT +AMURANT +AMUREE +AMURERENT +AMURONS +AMUSANTS +AMUSEE +AMUSERAIT +AMUSETTES +AMUSONS +AMYGDALINES +AMYLACES +AMYLOBACTERS +AMYOTROPHIES +ANABIOSE +ANABOLISEE +ANACOLUTHES +ANACRUSE +ANAGALLIS +ANAGNOSTES +ANALEPSES +ANALITE +ANALOGISMES +ANALYCITES +ANALYSANTE +ANALYSATES +ANALYSERAIT +ANALYSEURS +ANALYTICITES +ANAMORPHOSE +ANAPHASES +ANAR +ANARCHISME +ANASTATIQUE +ANASTOMOSA +ANASTOMOSES +ANASTYLOSES +ANATHEMES +ANATHEMISERA +ANATOCISME +ANATOMIQUES +ANATOMISER +ANATOXINE +ANCESTRAUX +ANCIEN +ANCOLIES +ANCRAIS +ANCRAT +ANCRERAIS +ANCREZ +ANDAINAI +ANDAINASSIEZ +ANDAINERAI +ANDAINERONT +ANDALOUS +ANDINES +ANDOUILLERS +ANDROCEPHALE +ANDROGENIQUE +ANDROLOGIE +ANDROPOGON +ANEANTIMES +ANEANTIRIEZ +ANECDOTE +ANECDOTISAT +ANECDOTISONS +ANEMIAI +ANEMIASSE +ANEMIENT +ANEMIERIEZ +ANEMIQUES +ANEMONES +ANERGISANTE +ANESTHESIA +ANESTHESIAS +ANESTHESIENT +ANEVRISMALE +ANEVRYSMES +ANGELIQUE +ANGELISAS +ANGELISEES +ANGELISEREZ +ANGELISMES +ANGIECTASIE +ANGIOLOGUES +ANGIOSCOPIE +ANGKORIENNES +ANGLAISAMES +ANGLAISE +ANGLAISERAS +ANGLAISIONS +ANGLICANES +ANGLICISAIT +ANGLICISER +ANGLICISTE +ANGLOPHOBE +ANGOISSAIT +ANGOISSERAI +ANGOISSERONT +ANGONS +ANGRAECUMS +ANGUILLERES +ANGUILLULOSE +ANHELAI +ANHELASSIEZ +ANHELERAI +ANHELERONT +ANHIDROTIQUE +ANHYDRITE +ANICIENS +ANILIDE +ANIMALERIE +ANIMALISAIS +ANIMALISAT +ANIMALISEZ +ANIMASSENT +ANIMATO +ANIMER +ANIMERIONS +ANIMISTES +ANISAI +ANISASSE +ANISENT +ANISERIEZ +ANISIENNES +ANISONS +ANKARIENNE +ANKYLOSAMES +ANKYLOSERA +ANKYLOSERONS +ANNALITE +ANNECIENNE +ANNELAMES +ANNELE +ANNELIDES +ANNELLERAS +ANNEXA +ANNEXASSES +ANNEXERA +ANNEXERONS +ANNEXIONS +ANNIHILANT +ANNIHILERAIT +ANNIHILIEZ +ANNONCAI +ANNONCASSIEZ +ANNONCERAI +ANNONCERONT +ANNONE +ANNOTAIENT +ANNOTASSIONS +ANNOTEE +ANNOTERENT +ANNOTONS +ANNUALISAS +ANNUALISE +ANNUALISERAS +ANNUALISIONS +ANNUITES +ANNULAIS +ANNULAT +ANNULEES +ANNULEREZ +ANOBIE +ANOBLIRAI +ANOBLIRONT +ANOBLISSES +ANODINE +ANODISAMES +ANODISATION +ANODISERAIS +ANODISEZ +ANOMAL +ANOMALONS +ANOMOURE +ANONIERS +ANONNASSENT +ANONNEMENTS +ANONNEREZ +ANONS +ANONYMISAIT +ANONYMISATES +ANONYMISES +ANORDIMES +ANORDIRIEZ +ANORDISSENT +ANOREXIGENE +ANORMAL +ANOSMIQUES +ANSEES +ANTABUSE +ANTAGONISANT +ANTAGONISEES +ANTAGONISONS +ANTANACLASE +ANTECEDENCE +ANTEDILUVIEN +ANTENAIS +ANTENNATE +ANTEPOSA +ANTEPOSASSES +ANTEPOSERA +ANTEPOSERONS +ANTERIORITES +ANTHEMIS +ANTHOCYANE +ANTHOZOAIRE +ANTHRACITEUX +ANTHRISQUE +ANTHROPISAT +ANTHROPISES +ANTHROPOIDE +ANTHYLLIDE +ANTIAERIENNE +ANTICABREUR +ANTICHAMBRES +ANTICHRETIEN +ANTICIPANT +ANTICIPAT +ANTICIPER +ANTICODON +ANTICYCLIQUE +ANTIDATA +ANTIDATASSES +ANTIDATERA +ANTIDATERONS +ANTIDOPAGES +ANTIDROGUE +ANTIFADINGS +ANTIFORME +ANTIGEL +ANTIGREVES +ANTIGUERRE +ANTIJUIF +ANTIMERIDIEN +ANTIMISSILE +ANTIMONIES +ANTINAZISME +ANTINOMIQUES +ANTIOXYDANTS +ANTIPASTI +ANTIPHERNAL +ANTIPIRATAGE +ANTIPODISTES +ANTIPROTON +ANTIPUBS +ANTIQUAIRES +ANTIQUITE +ANTIRADAR +ANTIROUILLES +ANTISEMITES +ANTISEXISTE +ANTISOCIALES +ANTISPORTIF +ANTISUDORALE +ANTITACHE +ANTITOUT +ANTITUMORAL +ANTIULCEREUX +ANTIVITAMINE +ANTONOMASE +ANTRALES +ANURIQUE +ANXIEUSE +AORTE +AOUTAI +AOUTASSIEZ +AOUTENT +AOUTERIEZ +AOUTIENS +APAGOGIES +APAISANTE +APAISATES +APAISERAIENT +APAISES +APANAGEAIT +APANAGEATES +APANAGERAIT +APANAGERS +APARTHEIDS +APATOSAURE +APERCEPTIBLE +APERCEVABLE +APERCEVONS +APERCEVRONS +APERCUMES +APERIODIQUE +APERO +APETISSAIENT +APETISSES +APEURAMES +APEURE +APEURERAS +APEURIONS +APHELIES +APHIDOIDES +APHYLLE +APICOLES +APIECEURS +APIGEONNAMES +APIGEONNE +APIGEONNERAS +APIGEONNIONS +APIQUAIENT +APIQUASSIONS +APIQUERAIENT +APIQUES +APITOIERA +APITOIERONT +APITOYASSE +APITOYER +APLANIE +APLANIRENT +APLANISSANT +APLANISSIONS +APLATIE +APLATIRENT +APLATISSAIS +APLATISSEZ +APLATS +APLOMBANT +APLOMBEE +APLOMBERENT +APLOMBONS +APOCALYPSE +APOCRISIAIRE +APODIDE +APOGEE +APOLLINIENNE +APOLOGIES +APOMORPHES +APORETIQUES +APOSTAIS +APOSTASIAMES +APOSTASIE +APOSTASIERAS +APOSTASIIONS +APOSTATS +APOSTERAIT +APOSTES +APOSTILLAS +APOSTILLEES +APOSTILLEREZ +APOSTIONS +APOSTROPHAI +APOSTROPHEZ +APOTHEQUE +APPAIRAGES +APPAIRASSES +APPAIRERA +APPAIRERONS +APPALACHIENS +APPARAISSE +APPARAT +APPAREILLADE +APPAREILLAS +APPAREILLEZ +APPARENTAI +APPARENTER +APPARIAI +APPARIASSIEZ +APPARIER +APPARIERIONS +APPARITION +APPARTENAIT +APPARTENIR +APPARTINS +APPARUE +APPARUT +APPATAMES +APPATE +APPATERAS +APPATIONS +APPAUVRIS +APPELABLES +APPELAS +APPELEES +APPELLATIFS +APPELLERAIS +APPELS +APPENDICE +APPENDICULES +APPENDRE +APPENDUS +APPERTISERA +APPESANTIMES +APPETISSANTE +APPLAUDIMES +APPLAUDIRENT +APPLICABLES +APPLICATIVES +APPLIQUAS +APPLIQUEES +APPLIQUEREZ +APPLIQUIONS +APPOINTAI +APPOINTERA +APPOINTERONS +APPOINTIR +APPOINTISSES +APPONDAIENT +APPONDIONS +APPONDONS +APPONDRIONS +APPONTA +APPONTASSE +APPONTENT +APPONTERIEZ +APPONTONS +APPORTASSE +APPORTENT +APPORTERIEZ +APPORTIEZ +APPOSANT +APPOSEE +APPOSERENT +APPOSITION +APPRECIAIS +APPRECIAT +APPRECIERAIS +APPRECIEZ +APPREHENDANT +APPREHENDEES +APPREHENSIFS +APPRENANTES +APPRENDREZ +APPRENNENT +APPRETA +APPRETASSE +APPRETENT +APPRETERIEZ +APPRETIEZ +APPRISSENT +APPRIVOISAT +APPRIVOISES +APPROBATEURS +APPROBATRICE +APPROCHANT +APPROCHAT +APPROCHERAIS +APPROCHEZ +APPROFONDIS +APPROFONDIT +APPROPRIASSE +APPROPRIEE +APPROPRIONS +APPROUVAS +APPROUVEES +APPROUVEREZ +APPUIERAIS +APPUYA +APPUYASSES +APPUYES +APREMS +APRIORITE +APTERES +APTITUDES +APURAS +APUREES +APURERAS +APURIONS +APYROGENE +AQUAFORTISTE +AQUAPLANE +AQUARELLANT +AQUARELLEE +AQUARELLISTE +AQUATINTES +AQUAZOLES +AQUICULTRICE +AQUILIN +ARABETTES +ARABISAMES +ARABISERA +ARABISERONS +ARABITES +ARACHIDE +ARACHNIDE +ARACHNOPHOBE +ARAGONITE +ARALIA +ARAMON +ARAS +ARASASSENT +ARASEMENTS +ARASEREZ +ARATOIRE +ARBALETE +ARBITRAGES +ARBITRAL +ARBITRASSIEZ +ARBITRERA +ARBITRERONS +ARBORAIENT +ARBORASSIONS +ARBORERAIENT +ARBORES +ARBORICOLE +ARBORISA +ARBORISASSES +ARBORISENT +ARBORISERIEZ +ARBOUSE +ARBRIERS +ARBUSTIVES +ARCANE +ARCBOUTA +ARCBOUTER +ARCEAU +ARCHAISMES +ARCHEEN +ARCHEOPTERYX +ARCHES +ARCHETYPALE +ARCHEVEQUES +ARCHIDIACONE +ARCHIDUCAL +ARCHIFAVORIS +ARCHIPLEINE +ARCHIVAGES +ARCHIVASSES +ARCHIVERA +ARCHIVERONS +ARCHIVOLTE +ARCONNAI +ARCONNASSIEZ +ARCONNERAI +ARCONNERONT +ARCURE +ARDENNAIS +ARDILLONS +ARDOISASSE +ARDOISENT +ARDOISERIE +ARDOISIERES +AREAGE +AREIQUES +ARENACES +ARENEUSES +AREOGRAPHIE +AREOMETRIQUE +ARES +ARETINS +ARGELESIENNE +ARGENTAIENT +ARGENTASSES +ARGENTENT +ARGENTERIE +ARGENTEURS +ARGENTINE +ARGENTS +ARGILACES +ARGIOPE +ARGONNAISE +ARGOTISME +ARGOUSINS +ARGUAMES +ARGUE +ARGUERAS +ARGUIONS +ARGUMENTANT +ARGUMENTE +ARGUMENTERAS +ARGUMENTIONS +ARGYRIES +ARHATS +ARIDITES +ARILLE +ARISAIT +ARISATES +ARISERAIT +ARISIEZ +ARKOSE +ARLESIENNES +ARMAI +ARMASSENT +ARMATURE +ARMENIENNE +ARMERAS +ARMET +ARMINIANISME +ARMOIRES +ARMORIAIT +ARMORIASSIEZ +ARMORIEE +ARMORIERENT +ARMORIONS +ARNAQUAI +ARNAQUASSIEZ +ARNAQUERAI +ARNAQUERONT +ARNICA +AROIDEES +AROMATISA +AROMATISAS +AROMATISE +AROMATISERAS +AROMATISIONS +ARPEGE +ARPEGEASSENT +ARPEGER +ARPEGERIONS +ARPENTAGES +ARPENTASSES +ARPENTERA +ARPENTERONS +ARPENTONS +ARQUAIENT +ARQUASSIONS +ARQUEBUSIER +ARQUERAIT +ARQUIEZ +ARRACHAMES +ARRACHE +ARRACHERAIS +ARRACHEUR +ARRACHONS +ARRAISONNANT +ARRAISONNEES +ARRAISONNONS +ARRANGEANT +ARRANGEAT +ARRANGERAI +ARRANGERONT +ARRENTAI +ARRENTASSIEZ +ARRENTERAI +ARRENTERONT +ARRERAGEAIS +ARRERAGEAT +ARRERAGERAIS +ARRERAGEZ +ARRETAIENT +ARRETASSIONS +ARRETERAIENT +ARRETES +ARRHENOTOQUE +ARRIERAS +ARRIERE +ARRIERERAS +ARRIERIONS +ARRIMANT +ARRIMEE +ARRIMERENT +ARRIMEUSES +ARRISAMES +ARRISE +ARRISERAS +ARRISIONS +ARRIVANT +ARRIVAT +ARRIVERAIS +ARRIVEZ +ARROBASES +ARROGANTES +ARROGEAS +ARROGENT +ARROGEREZ +ARROIS +ARRONDIRAIT +ARRONDISSEUR +ARROSA +ARROSANT +ARROSEE +ARROSERAIT +ARROSEURS +ARROYOS +ARSENICAUX +ARSINS +ARTEMIA +ARTERIEL +ARTESIENNES +ARTHRITISME +ARTHROPATHIE +ARTHROSIQUE +ARTICHE +ARTICULAIT +ARTICULATES +ARTICULENT +ARTICULERIEZ +ARTICULONS +ARTIFICIEUX +ARTIOZOAIRES +ARTISANES +ARTOCARPE +ARVALE +ARYANISAIENT +ARYANISERA +ARYANISERONS +ARYENS +ASADOS +ASCARIDES +ASCENDANTES +ASCENSIONNEE +ASCETE +ASCITIQUE +ASCORBIQUES +ASEMANTICITE +ASEPTISAIENT +ASEPTISERA +ASEPTISERONS +ASEXUE +ASHANTIES +ASIADOLLARS +ASICS +ASINIEN +ASNIEROISE +ASPARTIQUES +ASPERGEAI +ASPERGERAI +ASPERGERIONS +ASPERGILLUS +ASPERSEUR +ASPHALTAGE +ASPHALTER +ASPHALTIQUES +ASPHYXIAIENT +ASPHYXIER +ASPI +ASPIRAIL +ASPIRASSENT +ASPIRATION +ASPIREE +ASPIRERENT +ASPIRINES +ASQUE +ASSAGIRAI +ASSAGIRONT +ASSAGISSES +ASSAILLAIT +ASSAILLIE +ASSAILLIRAIT +ASSAINIMES +ASSAINIRIEZ +ASSAINIT +ASSAMAISE +ASSARMENTANT +ASSARMENTEES +ASSASSINA +ASSASSINER +ASSAUTS +ASSECHAIT +ASSECHATES +ASSECHES +ASSEMBLASSES +ASSEMBLENT +ASSEMBLERIEZ +ASSEMBLIEZ +ASSENAS +ASSENEES +ASSENEREZ +ASSENTIMENT +ASSERMENTAS +ASSERMENTEE +ASSERTIF +ASSERVIES +ASSERVIREZ +ASSESSEURE +ASSETTES +ASSEYIONS +ASSIBILAS +ASSIBILE +ASSIBILERAS +ASSIBILIONS +ASSIEDS +ASSIEGEANTES +ASSIEGEE +ASSIEGERAS +ASSIEGIONS +ASSIERONS +ASSIGNAIENT +ASSIGNER +ASSIGNERIONS +ASSIMILABLE +ASSIMILANTS +ASSIMILENT +ASSIMILERIEZ +ASSIS +ASSISTAI +ASSISTANTE +ASSISTATES +ASSISTERAIT +ASSISTIEZ +ASSOCIAIT +ASSOCIATES +ASSOCIEE +ASSOCIERENT +ASSOCIONS +ASSOIFFANT +ASSOIFFEE +ASSOIFFERENT +ASSOIFFONS +ASSOIRONS +ASSOLANT +ASSOLEE +ASSOLERAIT +ASSOLIEZ +ASSOMBRIS +ASSOMMAMES +ASSOMMES +ASSOMMONS +ASSONAIENT +ASSONANTES +ASSONE +ASSONEREZ +ASSORTI +ASSORTIRAIS +ASSORTISSONS +ASSOUPIS +ASSOUPLIE +ASSOUPLIRENT +ASSOURDIES +ASSOURDIREZ +ASSOUVIS +ASSOUVISSEZ +ASSOYEZ +ASSUJETTIR +ASSUMAIENT +ASSUMASSIONS +ASSUMERAIENT +ASSUMES +ASSURAI +ASSURANCIELS +ASSURASSIEZ +ASSURERA +ASSURERONS +ASSYRIENNE +ASTACICOLES +ASTARTE +ASTERACEES +ASTERISQUES +ASTICOTAIENT +ASTICOTES +ASTICS +ASTIQUAGE +ASTIQUASSENT +ASTIQUER +ASTIQUERIONS +ASTRAGALES +ASTRAUX +ASTREIGNENT +ASTREINDRAIT +ASTREINTE +ASTRINGENTES +ASTROCYTOME +ASTROLOGIQUE +ASTRONOME +ASTURIEN +ATACA +ATAVISME +ATELES +ATEMPORELLE +ATERMOIENT +ATERMOIERONS +ATERMOYAS +ATERMOYEES +ATHANOR +ATHENEE +ATHETOSES +ATHREPSIE +ATHYROIDIE +ATLANTHROPE +ATLAS +ATOCAS +ATOMICITES +ATOMISAS +ATOMISE +ATOMISERAS +ATOMISEZ +ATONAL +ATONES +ATOURS +ATRAZINE +ATROCE +ATROPHIAMES +ATROPHIES +ATROPINIQUES +ATTABLANT +ATTABLEE +ATTABLERENT +ATTABLONS +ATTACHANTS +ATTACHEE +ATTACHERAIT +ATTACHIEZ +ATTAQUAIENT +ATTAQUASSENT +ATTAQUER +ATTAQUERIONS +ATTARDAIENT +ATTARDES +ATTEIGNANT +ATTEIGNISSE +ATTEINS +ATTELAI +ATTELASSIEZ +ATTELET +ATTELLERAIS +ATTELLOIRES +ATTEND +ATTENDIMES +ATTENDITES +ATTENDRI +ATTENDRIRAIS +ATTENDRISSES +ATTENDRONT +ATTENTAIT +ATTENTATES +ATTENTERAIS +ATTENTEZ +ATTENTIVES +ATTENUANTES +ATTENUATEUR +ATTENUERAI +ATTENUERONT +ATTERRAIENT +ATTERRASSENT +ATTERREMENTS +ATTERREREZ +ATTERRIEZ +ATTERRIRENT +ATTERRISSAIS +ATTERRISSEZ +ATTESTAIS +ATTESTAT +ATTESTERAI +ATTESTERONT +ATTIEDI +ATTIEDIRAS +ATTIEDISSAIT +ATTIEDISSONS +ATTIFAIS +ATTIFAT +ATTIFERAI +ATTIFERONT +ATTIGEAI +ATTIGEASSIEZ +ATTIGERAI +ATTIGERONT +ATTIKAMEQUES +ATTIRAIS +ATTIRASSE +ATTIRENT +ATTIRERIEZ +ATTISAI +ATTISASSIEZ +ATTISER +ATTISERIONS +ATTITRA +ATTITRASSES +ATTITRERA +ATTITRERONS +ATTITUDINALE +ATTRACTIFS +ATTRAIENT +ATTRAIRIEZ +ATTRAPADE +ATTRAPAS +ATTRAPEES +ATTRAPEREZ +ATTRAPEZ +ATTRAYANTS +ATTREMPAIS +ATTREMPAT +ATTREMPERAIS +ATTREMPEZ +ATTRIBUAIT +ATTRIBUATES +ATTRIBUERAIT +ATTRIBUIEZ +ATTRIBUTIVE +ATTRIQUAS +ATTRIQUEES +ATTRIQUEREZ +ATTRISTA +ATTRISTAS +ATTRISTEES +ATTRISTEREZ +ATTRITION +ATTROUPASSE +ATTROUPEMENT +ATTROUPERENT +ATTROUPONS +AUBAGE +AUBERES +AUBERONS +AUBINAI +AUBINASSIEZ +AUBINERAI +AUBINERONT +AUBRAC +AUBURNIENS +AUDACIEUSE +AUDIENCAIENT +AUDIENCERA +AUDIENCERONS +AUDIENCONS +AUDIMUTITES +AUDIOGUIDAGE +AUDIOGUIDERA +AUDIOMETRE +AUDIOPHONE +AUDITAIENT +AUDITASSIONS +AUDITERAIENT +AUDITES +AUDITIONNAT +AUDITIONNIEZ +AUDITORATS +AUDOISES +AUGEES +AUGITE +AUGMENTAMES +AUGMENTATEUR +AUGMENTE +AUGMENTERAS +AUGMENTIONS +AUGURALES +AUGURATES +AUGURERAIENT +AUGURES +AUGUSTES +AUGUSTINS +AULNAISIENNE +AUMAILLE +AUMUSSE +AUNAIT +AUNATES +AUNERAIT +AUNIEZ +AURA +AURELIE +AUREOLANT +AUREOLEE +AUREOLERENT +AUREOLONS +AURICULES +AURIFIANT +AURIFIERAIT +AURIFIIEZ +AURILLACOISE +AURORAL +AUSCULTA +AUSCULTASSES +AUSCULTERAIT +AUSCULTIEZ +AUSTENITES +AUSTRALES +AUSTRAUX +AUTARCIQUE +AUTHENTIFIA +AUTHENTIFIAT +AUTHENTIQUES +AUTISTIQUES +AUTOANALYSEE +AUTOANTIGENE +AUTOBLOQUANT +AUTOBUS +AUTOCENTREES +AUTOCHTONE +AUTOCLAVAGE +AUTOCOPIE +AUTOCRATE +AUTODEFENSES +AUTODETRUITE +AUTODIDAXIES +AUTODROMES +AUTOFINANCE +AUTOGAMIE +AUTOGERAIT +AUTOGERATES +AUTOGERERAIT +AUTOGERIEZ +AUTOGRAPHES +AUTOGUIDAGES +AUTOMATICIEN +AUTOMATIQUES +AUTOMATISENT +AUTOMEDIQUEZ +AUTOMNE +AUTOMUTILAI +AUTOMUTILERA +AUTONOMISAIT +AUTONOMISIEZ +AUTONYMIES +AUTOPHAGIES +AUTOPONTS +AUTOPORTRAIT +AUTOPRODUIS +AUTOPSIASSE +AUTOPSIENT +AUTOPSIERIEZ +AUTOPUNITIFS +AUTOREGULAS +AUTOREGULIEZ +AUTORISAMES +AUTORISATION +AUTORISERAIS +AUTORISEZ +AUTOSOMIQUE +AUTOTRACTEE +AUTOTROPHIE +AUTOVACCINS +AUTRICHIENS +AUVENT +AUXDITS +AUXILIATEURS +AVACHIE +AVACHIRENT +AVACHISSANT +AVACHIT +AVALAISON +AVALANTE +AVALATES +AVALERAIENT +AVALES +AVALISAIENT +AVALISES +AVALISTES +AVANCAIS +AVANCAT +AVANCERAI +AVANCERONT +AVANTAGEA +AVANTAGERA +AVANTAGERONS +AVANTS +AVARIANT +AVARICES +AVARIERAI +AVARIERONT +AVELINE +AVENEMENTS +AVENTURAIT +AVENTURATES +AVENTURERAIT +AVENTURINES +AVENUS +AVERASSENT +AVERER +AVERERIONS +AVERROISTE +AVERTIMES +AVERTIRIEZ +AVERTISSIONS +AVEUGLAIS +AVEUGLASSES +AVEUGLENT +AVEUGLERIEZ +AVEULI +AVEULIRAS +AVEULISSAIT +AVEULISSONS +AVIATEUR +AVICULE +AVIDES +AVIGNONNAIS +AVILIRAIENT +AVILIS +AVINAGE +AVINASSENT +AVINER +AVINERIONS +AVIONIQUES +AVIRON +AVIRONNERAI +AVIRONNERONT +AVIRULENTS +AVISASSE +AVISENT +AVISERIEZ +AVISO +AVITAILLAS +AVITAILLEES +AVITAILLERAS +AVITAILLEZ +AVIVAIENT +AVIVASSIONS +AVIVERA +AVIVERONS +AVOCASSAI +AVOCASSERAIS +AVOCASSERONT +AVOCAT +AVOIE +AVOIERIONS +AVOINANT +AVOINEE +AVOINERENT +AVOINONS +AVOISINAMES +AVOISINES +AVORTAIT +AVORTATES +AVORTERAIENT +AVORTES +AVOUA +AVOUASSE +AVOUENT +AVOUERIEZ +AVOYAI +AVOYASSIEZ +AVOYEZ +AVUNCULAT +AXAIS +AXAT +AXENIQUES +AXERAS +AXEZ +AXIOLOGIE +AXIOMATISAIT +AXIOMATISIEZ +AXONGE +AYATOLLAH +AYURVEDAS +AZIMUTAUX +AZOLLA +AZORAIENT +AZORASSIONS +AZORERAIENT +AZORES +AZOTAMES +AZOTATES +AZOTERA +AZOTERONS +AZOTIONS +AZOTURIE +AZUR +AZURANTE +AZURATES +AZURERA +AZURERONS +AZYGOS +BABALLES +BABEURRE +BABILLAIENT +BABILLASSE +BABILLENT +BABILLERIEZ +BABINE +BABOLAIENT +BABOLASSIONS +BABOLERAIT +BABOLIEZ +BABOUCHKA +BABOUVISME +BABYSITTER +BACCARAS +BACCIFORME +BACHAIT +BACHATES +BACHELORS +BACHEREZ +BACHIQUES +BACHOTAGE +BACHOTASSENT +BACHOTER +BACHOTERIONS +BACHOTIONS +BACILLIFERES +BACKGAMMONS +BACLAI +BACLASSIEZ +BACLERAI +BACLERONT +BACON +BACTERICIDES +BACTERIENNES +BACULAS +BADAMIERS +BADAUD +BADAUDASSENT +BADAUDERAI +BADAUDERIONS +BADECHE +BADENT +BADERIEZ +BADGEAI +BADGEASSIEZ +BADGERAI +BADGERONT +BADIGEON +BADIGEONNAS +BADIGEONNENT +BADINAIS +BADINAT +BADINERAS +BADINEZ +BADOISE +BAFFA +BAFFASSES +BAFFERA +BAFFERONS +BAFOUAI +BAFOUASSIEZ +BAFOUERAI +BAFOUERONT +BAFOUILLAIT +BAFOUILLATES +BAFOUILLEURS +BAFRA +BAFRASSES +BAFRERA +BAFRERONS +BAFRONS +BAGARRAIENT +BAGARRES +BAGASSES +BAGDADIENNE +BAGNARD +BAGOUSE +BAGUAIENT +BAGUASSIONS +BAGUENAUDAT +BAGUENAUDES +BAGUERAI +BAGUERONT +BAGUIO +BAHAMEENNE +BAHREINIENNE +BAHUTS +BAIEREZ +BAIGNAIENT +BAIGNASSIONS +BAIGNERAIENT +BAIGNES +BAIGNONS +BAILLANT +BAILLEE +BAILLERAIT +BAILLES +BAILLIES +BAILLONNAIS +BAILLONNAT +BAILLONNERAI +BAINS +BAISABLES +BAISASSES +BAISEMENT +BAISERENT +BAISEUSE +BAISOTAIENT +BAISOTES +BAISSAMES +BAISSE +BAISSERAS +BAISSIERE +BAJOCIENNES +BAKEOFE +BALADAIS +BALADAT +BALADERAIS +BALADEUR +BALADIONS +BALAFRAIS +BALAFRAT +BALAFRERAIS +BALAFREZ +BALAIERAIS +BALAISE +BALANCAMES +BALANCE +BALANCERAI +BALANCERONT +BALANCOIRES +BALANOGLOSSE +BALAYAGE +BALAYASSENT +BALAYER +BALAYERIONS +BALAYEZ +BALBUTIAIS +BALBUTIASSES +BALBUTIENT +BALBUTIERIEZ +BALBUZARDS +BALCONS +BALEINEAUX +BALENIDE +BALEVRES +BALISAIENT +BALISASSIONS +BALISERAIENT +BALISES +BALISONS +BALISTITES +BALIVAS +BALIVEAUX +BALIVERAS +BALIVEZ +BALKANISAIT +BALKANISATES +BALKANISES +BALLAIS +BALLASSES +BALLASTAIT +BALLASTATES +BALLASTERAIT +BALLASTIERE +BALLER +BALLERINE +BALLETTOMANE +BALLONNAIT +BALLONNATES +BALLONNES +BALLOTES +BALLOTTAIENT +BALLOTTERA +BALLOTTERONS +BALLOUNES +BALNEOS +BALOURD +BALS +BALSAS +BALUBAS +BALZACIENNE +BAMBA +BAMBOCHADES +BAMBOCHARDS +BAMBOCHENT +BAMBOCHERIEZ +BAMBOCHIEZ +BANAL +BANALISANT +BANALISERAIT +BANALISIEZ +BANANAIT +BANANATES +BANANERAIES +BANANES +BANASTES +BANCAIRES +BANCARISAMES +BANCARISEZ +BANCHAGES +BANCHASSES +BANCHERA +BANCHERONS +BANCOULIER +BANDAGISTES +BANDANTES +BANDE +BANDERAI +BANDEZ +BANDITS +BANDOULIERE +BANGKOKIENS +BANGUISSOISE +BANJULAISE +BANNE +BANNIERE +BANNIRAS +BANNISSAIENT +BANNISSIEZ +BANQUAI +BANQUASSIEZ +BANQUERAI +BANQUERONT +BANQUETAI +BANQUETEUSES +BANQUEZ +BANQUISTES +BANTUS +BAOULES +BAPTISAS +BAPTISEES +BAPTISEREZ +BAPTISMALE +BAPTISTERES +BAQUASSE +BAQUENT +BAQUERIEZ +BAQUETAIT +BAQUETATES +BAQUETONS +BAQUETTEREZ +BAQUONS +BARAGOUINAIT +BARAGOUINE +BARAQUAIENT +BARAQUERA +BARAQUERONS +BARATINA +BARATINASSES +BARATINERA +BARATINERONS +BARATINONS +BARATTANT +BARATTEE +BARATTERENT +BARATTONS +BARBAI +BARBAQUES +BARBARISA +BARBARISERA +BARBASSE +BARBECUE +BARBENT +BARBERIEZ +BARBICHE +BARBIERES +BARBIFIANTE +BARBIFIATES +BARBIFIERAIT +BARBIFIIEZ +BARBITURIQUE +BARBOTAGE +BARBOTASSENT +BARBOTER +BARBOTERIONS +BARBOTIERES +BARBOTTES +BARBOUILLAS +BARBOUILLENT +BARBUDIENNES +BARCASSES +BARDAGES +BARDASSA +BARDASSASSES +BARDASSERA +BARDASSERONS +BARDEAU +BARDERAIENT +BARDES +BARDOT +BAREMIQUES +BARETAS +BARETER +BARETERIONS +BARGUIGNA +BARGUIGNASSE +BARGUIGNERA +BARGUIGNONS +BARIOLA +BARIOLASSE +BARIOLENT +BARIOLERIEZ +BARIOLURES +BARJAQUAMES +BARJAQUE +BARJAQUEREZ +BARJO +BARLOTIERE +BARNACHE +BARODETS +BARONNIAL +BAROQUEUSE +BAROSCOPES +BAROUDANT +BAROUDENT +BAROUDERIEZ +BAROUDIEZ +BAROULA +BAROULASSES +BAROULERA +BAROULERONS +BARQUES +BARRAI +BARRASSENT +BARREES +BARRENT +BARRERIEZ +BARRETTES +BARRICADAIS +BARRICADAT +BARRICADEZ +BARRIONS +BARRIRENT +BARRISSANT +BARRIT +BARSACS +BARTONIENNE +BARULAS +BARULEES +BARULEREZ +BARYCENTRE +BARYONIQUES +BARYTITE +BAS +BASALTES +BASANANT +BASANEE +BASANERENT +BASANITE +BASAT +BASCULAMES +BASCULERA +BASCULERONS +BASEBALL +BASENT +BASERIEZ +BASICS +BASILAIRE +BASILIQUES +BASIR +BASIRIONS +BASISSE +BASITES +BASMATIS +BASOPHILE +BASQUETS +BASSET +BASSIER +BASSINAIS +BASSINASSES +BASSINERA +BASSINERONS +BASSINOIRE +BASTA +BASTANT +BASTATES +BASTERENT +BASTIAIS +BASTILLEES +BASTION +BASTIONNER +BASTONNA +BASTONNASSE +BASTONNENT +BASTONNERIEZ +BASTOS +BATAILLAI +BATAILLERAIS +BATAILLEUR +BATAIT +BATARDS +BATAVES +BATE +BATELAIS +BATELAT +BATELEUR +BATELIONS +BATELLERIE +BATERA +BATERONS +BATHONIENS +BATIDAS +BATIFOLAIENT +BATIFOLERAIT +BATIFOLEURS +BATILLAGES +BATIRAIT +BATISSABLES +BATISSEUSE +BATNEEN +BATOILLANT +BATOILLENT +BATOILLERIEZ +BATONNA +BATONNANT +BATONNE +BATONNERAS +BATONNEZ +BATONS +BATTAIENT +BATTEE +BATTEUR +BATTISSE +BATTOIRS +BATTRE +BATTURES +BAUDET +BAUGEA +BAUGEASSES +BAUGERA +BAUGERONS +BAUME +BAVAI +BAVARDAI +BAVARDASSIEZ +BAVARDERAIS +BAVARDEZ +BAVASSAI +BAVASSASSIEZ +BAVASSERAIS +BAVASSEZ +BAVER +BAVERIONS +BAVEUX +BAVOCHANT +BAVOCHEE +BAVOCHERENT +BAVOCHONS +BAYA +BAYASSE +BAYERA +BAYERONS +BAYONNAISES +BAZADAISES +BAZARDASSE +BAZARDENT +BAZARDERIEZ +BAZARIS +BEAGLE +BEANTE +BEASSIEZ +BEATIFIAIT +BEATIFIATES +BEATIFIES +BEATNIKS +BEAUFORTS +BEBITE +BECASSEAU +BECHA +BECHANT +BECHEE +BECHERENT +BECHEUSE +BECHEVETASSE +BECHEVETENT +BECHEVETTENT +BECOSSES +BECOTASSE +BECOTENT +BECOTERIEZ +BECQUEE +BECQUETAIS +BECQUETERAI +BECQUETERONT +BECQUETTERAI +BECQUETTES +BECTANT +BECTEE +BECTERENT +BECTONS +BEDEISTE +BEDONNAIENT +BEDONNASSENT +BEDONNERAI +BEDONNERONT +BEDOUINS +BEERAIENT +BEES +BEGAIEMENTS +BEGAIERIONS +BEGAYAIT +BEGAYASSIEZ +BEGAYER +BEGAYERIONS +BEGAYIONS +BEGUARDS +BEGUETAS +BEGUETEMENTS +BEGUETEREZ +BEGUEULE +BEGUM +BEIGEATRES +BEIRAMS +BELAIT +BELASSENT +BELEES +BELERAIS +BELETTE +BELGEOISANT +BELIER +BELINOS +BELLATRE +BELLUAIRES +BELOTEUSES +BEMOL +BEMOLISER +BENARD +BENEDICTINE +BENEFICIERS +BENETS +BENGALIS +BENIGNITES +BENIRA +BENIRONS +BENISSEUR +BENITES +BENJOIN +BENTHIQUE +BENZENES +BENZOLS +BENZYLES +BEOTIENNES +BEQUETAIS +BEQUETAT +BEQUETERAIS +BEQUETEZ +BEQUETTERAIS +BEQUILLAGE +BEQUILLARDES +BEQUILLE +BEQUILLERAS +BEQUILLIONS +BERBERIS +BERCAIL +BERCASSENT +BERCEES +BERCERAIS +BERCEUR +BERDINES +BERGAMOTIER +BERGERETTE +BERGSONIEN +BERK +BERLINETTES +BERMUDA +BERNACLES +BERNARDINS +BERNEE +BERNERAIT +BERNEURS +BERNOIS +BERS +BERYL +BESACIER +BESET +BESOGNAMES +BESOGNE +BESOGNERAS +BESOGNEUX +BESSONNE +BESTIALES +BETATRON +BETIFIAI +BETIFIASSE +BETIFIENT +BETIFIERIEZ +BETISAI +BETISASSIEZ +BETISERAIS +BETISEZ +BETON +BETONNAS +BETONNEES +BETONNEREZ +BETONNEZ +BETTERAVIER +BETULINEES +BEUGLANTE +BEUGLATES +BEUGLERAIENT +BEUGLES +BEUGNAMES +BEUGNE +BEUGNERAS +BEUGNIONS +BEURRAGES +BEURRASSES +BEURRERA +BEURRERIEZ +BEURRIEZ +BEY +BEYROUTHINE +BIACUMINEE +BIAISAIS +BIAISAT +BIAISERAI +BIAISERONT +BIATHLETE +BIAURALES +BIBANDE +BIBELOTAI +BIBELOTERAIS +BIBELOTEUR +BIBENDUMS +BIBERONNASSE +BIBERONNENT +BIBI +BIBITTE +BIBLIOLOGIE +BIBLIOPHILE +BIBLIQUE +BICEPHALES +BICHANT +BICHELAMAR +BICHERENT +BICHIEZ +BICHLORURES +BICHONNAIT +BICHONNATES +BICHONNERAIT +BICHONNIEZ +BICIPITAL +BICONCAVE +BICOTS +BIDE +BIDONNAIT +BIDONNASSIEZ +BIDONNERAI +BIDONNERONT +BIDONS +BIDOUILLAMES +BIDOUILLE +BIDOUILLERAS +BIDOUILLEUSE +BIEFS +BIENFACTURE +BIENFAITEURS +BIENNAUX +BIENSEANTS +BIEVRE +BIFFAGES +BIFFASSES +BIFFENT +BIFFERIEZ +BIFFINS +BIFLECHE +BIFOLIOLE +BIFURQUA +BIFURQUASSES +BIFURQUES +BIGARADES +BIGARRAS +BIGARRERAIS +BIGARREZ +BIGEMINES +BIGLANT +BIGLEE +BIGLERENT +BIGLEZ +BIGNONIA +BIGOPHONANT +BIGOPHONEE +BIGOPHONONS +BIGORNANT +BIGORNEAU +BIGORNERAIT +BIGORNIEZ +BIGOTISMES +BIGOURDANES +BIGUES +BIJECTIFS +BIJOUTIERES +BILABIAL +BILAIS +BILASSE +BILEES +BILEREZ +BILHARZIA +BILIEES +BILINGUE +BILIVERDINES +BILLANT +BILLATES +BILLERA +BILLERONS +BILLETTERIE +BILLIONS +BILLONNANT +BILLONNEE +BILLONNERENT +BILLONNONS +BILOCALE +BILOQUAIENT +BILOQUES +BIMBELOTIER +BIMENSUELS +BIMETALLISME +BIMODAUX +BINAI +BINART +BINATIONAL +BINERA +BINERIEZ +BINEUSES +BINOCLARDE +BINOMIALE +BINTJES +BIOCHIMIES +BIODEGRADAIT +BIODEGRADIEZ +BIODYNAMIES +BIOETHIQUE +BIOLOGIE +BIOLOGISAMES +BIOLOGISES +BIOMATERIAUX +BIOMES +BIONGULES +BIOPHYSIQUES +BIOPOLES +BIORYTHMES +BIOSTASIE +BIOTECHNIQUE +BIOTHERAPIES +BIOXYDES +BIPANT +BIPARTISANS +BIPASSAIENT +BIPASSES +BIPEDIE +BIPERA +BIPERONS +BIPHASES +BIPLANS +BIPOLARISES +BIPS +BIRDIES +BIREMES +BIROUTES +BISAIEULS +BISANT +BISBILLES +BISCAYENS +BISCOTEAUX +BISCUITA +BISCUITASSES +BISCUITERA +BISCUITERIEZ +BISCUITONS +BISEAUTAIT +BISEAUTATES +BISEAUTERAIT +BISEAUTIEZ +BISERAIENT +BISES +BISEXUEL +BISIONS +BISMUTHINE +BISONTINE +BISQUAIS +BISQUAT +BISQUERAS +BISQUINE +BISSAIT +BISSASSE +BISSECTION +BISSERA +BISSERONS +BISSEXTILE +BISSEXUELLES +BISTORTES +BISTOURNAI +BISTOURNERAI +BISTRAIT +BISTRATES +BISTRERAIT +BISTRIEZ +BISTROTIER +BISULFURES +BITANGENTES +BITATES +BITENT +BITERIEZ +BITIONS +BITORDS +BITTAS +BITTEES +BITTEREZ +BITTONS +BITTURANT +BITTUREE +BITTURERENT +BITTURONS +BITUMAI +BITUMASSIEZ +BITUMERAI +BITUMERONT +BITUMINAIS +BITUMINAT +BITUMINERAIS +BITUMINEUSE +BITURAI +BITURASSIEZ +BITURER +BITURERIONS +BIUNIVOQUES +BIVEAUX +BIVOUAQUAI +BIVOUAQUEZ +BIZARRERIE +BIZETS +BIZOUS +BIZUTANT +BIZUTEE +BIZUTERENT +BIZUTEUSES +BLABLABLAS +BLABLATASSE +BLABLATERA +BLABLATERONS +BLACKBOULES +BLACKLISTEE +BLACKS +BLAGUAIS +BLAGUAT +BLAGUERAIS +BLAGUEUR +BLAIRAI +BLAIRASSIEZ +BLAIRER +BLAIRERIONS +BLAISOISE +BLAMAIT +BLAMATES +BLAMERAIT +BLAMIEZ +BLANCHET +BLANCHIR +BLANCHIRIONS +BLANCHISSEUR +BLANCHON +BLANQUISTES +BLASASSE +BLASEMENT +BLASERENT +BLASON +BLASONNER +BLASPHEMAI +BLASPHEME +BLASPHEMERAS +BLASPHEMIONS +BLASTOMERES +BLATERA +BLATERASSES +BLATERES +BLAZER +BLEDARDS +BLEMIRAI +BLEMIRONT +BLEMISSEMENT +BLENDE +BLEPHARITE +BLESAIT +BLESATES +BLESERAIT +BLESIEZ +BLESSAIENT +BLESSASSENT +BLESSER +BLESSERIONS +BLET +BLETSASSE +BLETSENT +BLETSERIEZ +BLETTES +BLETTIRAIT +BLETTISSAIS +BLETTISSIONS +BLEUE +BLEUI +BLEUIRAS +BLEUISSAIENT +BLEUISSENT +BLEUSAILLES +BLEUTASSENT +BLEUTER +BLEUTERIONS +BLIAUT +BLINDAGES +BLINDASSES +BLINDERA +BLINDERONS +BLINQUA +BLINQUAS +BLINQUEES +BLINQUEREZ +BLISTER +BLISTERISERA +BLIZZARD +BLOCK +BLOGS +BLOGUASSENT +BLOGUERAI +BLOGUERONT +BLOND +BLONDIE +BLONDINS +BLONDIRIEZ +BLONDISSENT +BLONDOIERA +BLONDOIERONT +BLONDOYASSE +BLONDOYEZ +BLOQUA +BLOQUAS +BLOQUEES +BLOQUEREZ +BLOQUEZ +BLOTTIES +BLOTTIREZ +BLOTTISSE +BLOTTITES +BLOUSANTS +BLOUSEE +BLOUSERENT +BLOUSON +BLUETOOTHS +BLUFFAMES +BLUFFASSIONS +BLUFFERAIENT +BLUFFES +BLUSH +BLUTAMES +BLUTE +BLUTERAS +BLUTEZ +BOBARDS +BOBEUR +BOBINAIT +BOBINASSIONS +BOBINERA +BOBINERONS +BOBINIER +BOBINOTS +BOBSLEIGHS +BOCAINE +BOCARDAIS +BOCARDAT +BOCARDERAIS +BOCARDEZ +BOCK +BODYBOARD +BOEINGS +BOEUFS +BOGHEADS +BOGOMILES +BOGUAMES +BOGUE +BOGUERAS +BOGUEZ +BOHRIUM +BOIRA +BOIRIONS +BOISAIT +BOISATES +BOISERAIENT +BOISERONS +BOISSEAUX +BOITA +BOITASSES +BOITERA +BOITERIEZ +BOITIERS +BOITILLANTES +BOITILLE +BOITILLERAS +BOITILLIONS +BOKIT +BOLCHEVISTES +BOLEROS +BOLIVARES +BOLLARD +BOLS +BOMBAIENT +BOMBARDAIS +BOMBARDAT +BOMBARDERAI +BOMBARDERONT +BOMBASIN +BOMBEE +BOMBERAIT +BOMBEURS +BOMBONS +BONAMIAS +BONASSERIE +BONBONS +BONDANT +BONDEE +BONDERAIT +BONDERISAIT +BONDERISATES +BONDERISES +BONDIEUSARD +BONDIRA +BONDIRONS +BONDISSE +BONDITES +BONDONNASSE +BONDONNENT +BONDONNERIEZ +BONDREE +BONHEURS +BONICHON +BONIFIAMES +BONIFICATION +BONIFIERAIS +BONIFIEZ +BONIMENTAIT +BONIMENTATES +BONITES +BONNET +BONNETIERS +BONNOTTES +BONTES +BOOGIE +BOOLEEN +BOOMERANG +BOOSTANT +BOOSTEE +BOOSTERENT +BOOSTIONS +BORA +BORANES +BORAZON +BORDAI +BORDASSIEZ +BORDELAISE +BORDELISANT +BORDELISEE +BORDELISONS +BORDERAIT +BORDERLINE +BORDIEZ +BORDURAIENT +BORDURES +BOREALES +BORIN +BORNAGES +BORNASSES +BORNERA +BORNERONS +BORNOIERA +BORNOIERONT +BORNOYAS +BORNOYEES +BOROSILICATE +BORRELIA +BORTSCH +BOSCO +BOSNIAQUE +BOSS +BOSSAS +BOSSEES +BOSSELAS +BOSSELEES +BOSSELLENT +BOSSELLERONS +BOSSERAIENT +BOSSES +BOSSOIR +BOSSUANT +BOSSUEE +BOSSUERENT +BOSSUONS +BOSTONNAIS +BOSTONNAT +BOSTONNERAS +BOSTONNIONS +BOTANISAIENT +BOTANISERAIT +BOTANISIEZ +BOTS +BOTTANT +BOTTEE +BOTTELANT +BOTTELEE +BOTTELIEZ +BOTTELLEREZ +BOTTERAI +BOTTERIONS +BOTTIERS +BOTULINIQUE +BOUBOU +BOUBOULERAI +BOUBOULERONT +BOUCAN +BOUCANAS +BOUCANEES +BOUCANEREZ +BOUCANIONS +BOUCHAGE +BOUCHARDA +BOUCHARDERA +BOUCHASSES +BOUCHERA +BOUCHERIE +BOUCHEURS +BOUCHOLEUR +BOUCHONNAMES +BOUCHONNE +BOUCHONNEZ +BOUCHOTEUR +BOUCHOYAS +BOUCHOYEES +BOUCLA +BOUCLASSE +BOUCLEMENT +BOUCLERENT +BOUCLIER +BOUDAIENT +BOUDASSIONS +BOUDDHISTES +BOUDERAIT +BOUDES +BOUDINAGE +BOUDINASSENT +BOUDINER +BOUDINERIONS +BOUDINS +BOUELA +BOUELASSES +BOUELERAIENT +BOUELES +BOUEUSE +BOUFFANTE +BOUFFASSIONS +BOUFFERAIENT +BOUFFES +BOUFFI +BOUFFIRAIS +BOUFFISSAGE +BOUFFISSIEZ +BOUGEASSE +BOUGEOIR +BOUGERAIT +BOUGIE +BOUGNAT +BOUGONNAMES +BOUGONNE +BOUGONNERAIS +BOUGONNERONT +BOUGONNONS +BOUGRINES +BOUILLAIT +BOUILLEE +BOUILLEZ +BOUILLIS +BOUILLOIRE +BOUILLONNE +BOUILLONNIEZ +BOUILLOTTANT +BOUILLOTTER +BOUINAIS +BOUINAT +BOUINERAIS +BOUINEZ +BOULA +BOULANGEAI +BOULANGERAI +BOULANGERIES +BOULANGISME +BOULASSES +BOULDOZEURS +BOULEGUAIENT +BOULEGUES +BOULERAIS +BOULET +BOULEUSE +BOULEVERSAI +BOULGOUR +BOULINE +BOULISME +BOULOCHAIS +BOULOCHAT +BOULOCHERAS +BOULOCHIONS +BOULONNAGE +BOULONNAS +BOULONNEES +BOULONNEREZ +BOULONNIONS +BOULOTTAMES +BOULOTTE +BOULOTTERAS +BOULOTTIONS +BOUMERANG +BOUQUETIER +BOUQUINAIENT +BOUQUINERONS +BOUQUINISTE +BOURBEUSES +BOURBONIENS +BOURDALOUE +BOURDONNER +BOURGE +BOURGEOISIES +BOURGEONNANT +BOURGS +BOURIATES +BOURLINGUAS +BOURLINGUERA +BOURONNA +BOURONNASSES +BOURONNES +BOURRAGE +BOURRASQUES +BOURRASSER +BOURRATIF +BOURRELAI +BOURRELERENT +BOURRELIONS +BOURRELLERIE +BOURRERA +BOURRERONS +BOURRICHE +BOURRIEZ +BOURROIR +BOURSETTE +BOURSICOTANT +BOURSICOTER +BOURSOUFFLEZ +BOURSOUFLAT +BOURSOUFLES +BOUSAIENT +BOUSASSIONS +BOUSCULAIS +BOUSCULAT +BOUSCULERAIS +BOUSCULEZ +BOUSERAI +BOUSERONT +BOUSILLAGE +BOUSILLER +BOUSILLIONS +BOUSTIFAILLA +BOUSTIFAILLE +BOUTAIS +BOUTASSENT +BOUTEFEU +BOUTENT +BOUTERIEZ +BOUTEURS +BOUTIQUIERES +BOUTONNAGES +BOUTONNASSES +BOUTONNENT +BOUTONNERIEZ +BOUTONNIERE +BOUTURA +BOUTURASSE +BOUTURENT +BOUTURERIEZ +BOUVERIES +BOUVETANT +BOUVETEE +BOUVETONS +BOUVETTEREZ +BOUVILLON +BOUZY +BOWAL +BOXAIS +BOXAT +BOXERAIS +BOXES +BOY +BOYAUTA +BOYAUTASSES +BOYAUTERA +BOYAUTERONS +BOYCOTTAGE +BOYCOTTER +BOYCOTTIONS +BRABANCONNE +BRACHIALE +BRACHYLOGIES +BRACONNAMES +BRACONNE +BRACONNERAS +BRACONNIERE +BRACTEE +BRADAIT +BRADATES +BRADERAIENT +BRADERONS +BRADONS +BRADYPES +BRAGUES +BRAHMANS +BRAIERAIENT +BRAILLA +BRAILLARDS +BRAILLEE +BRAILLERAIT +BRAILLEURS +BRAIRIONS +BRAISAIT +BRAISATES +BRAISERAIT +BRAISETTES +BRAMAIENT +BRAMASSIONS +BRAMERA +BRAMERONS +BRANCARDAGE +BRANCARDER +BRANCARDIONS +BRANCHAMES +BRANCHE +BRANCHERAIS +BRANCHETTE +BRANCHIONS +BRANDADES +BRANDILLAMES +BRANDILLE +BRANDILLERAS +BRANDILLIONS +BRANDIRENT +BRANDISSANT +BRANDON +BRANLAMES +BRANLASSIONS +BRANLERA +BRANLERONS +BRANLEZ +BRANTES +BRAQUAS +BRAQUEES +BRAQUERAIS +BRAQUET +BRAS +BRASAS +BRASEES +BRASEREZ +BRASIERS +BRASILLAIT +BRASILLATES +BRASILLERAIT +BRASILLIEZ +BRASQUAMES +BRASQUE +BRASQUERAS +BRASQUIONS +BRASSANT +BRASSATES +BRASSERAIT +BRASSES +BRASSEYAMES +BRASSEYE +BRASSEYERAS +BRASSEYIONS +BRASSIERES +BRAVAIT +BRAVATES +BRAVERAIS +BRAVERONT +BRAVOURE +BRAYASSE +BRAYENT +BRAYERIEZ +BRAYONS +BREAKAMES +BREAKDANCE +BREAKERAIS +BREAKES +BREBIS +BREDINS +BREDOUILLE +BREEDER +BREITSCHWANZ +BRELANS +BRELE +BRELERAS +BRELIONS +BRESAOLA +BRESILLAI +BRESILLERAI +BRESILLERONT +BRESSANE +BRETAILLAIT +BRETAILLATES +BRETAILLIEZ +BRETAUDAS +BRETAUDEES +BRETAUDEREZ +BRETECHE +BRETONNANTE +BRETTAIT +BRETTATES +BRETTELANT +BRETTELEE +BRETTELLERA +BRETTERAS +BRETTEUSE +BREUVAGE +BREVETAGE +BREVETASSENT +BREVETER +BREVETERIONS +BREVETS +BREVETTERIEZ +BREVITES +BRICHETONS +BRICOLAMES +BRICOLE +BRICOLERAS +BRICOLEUSE +BRIDA +BRIDASSE +BRIDENT +BRIDERIEZ +BRIDGEAIS +BRIDGEAT +BRIDGERAIS +BRIDGEUR +BRIDONS +BRIEFAS +BRIEFEES +BRIEFEREZ +BRIEFIONS +BRIFFAIS +BRIFFAT +BRIFFERAIS +BRIFFEZ +BRIGADISTE +BRIGANDAMES +BRIGANDE +BRIGANDERAS +BRIGANDINE +BRIGHTIQUES +BRIGUAMES +BRIGUE +BRIGUERAS +BRIGUIONS +BRILLAMMENT +BRILLANTAIT +BRILLANTATES +BRILLANTEURS +BRILLANTINAS +BRILLANTINES +BRILLAS +BRILLER +BRILLERIONS +BRIMADES +BRIMASSES +BRIMBALAMES +BRIMBALERA +BRIMBALERONS +BRIMBORIONS +BRIMERAIT +BRIMIEZ +BRINELL +BRINGUAIENT +BRINGUEBALEE +BRINGUEE +BRINGUERENT +BRINGUONS +BRIOCHEE +BRIOUATE +BRIQUASSE +BRIQUENT +BRIQUERIEZ +BRIQUETAIENT +BRIQUETES +BRIQUETTE +BRISAI +BRISANTS +BRISCARDS +BRISERAIENT +BRISES +BRISKAS +BRISTOL +BRIVISTES +BROCANTAIT +BROCANTATES +BROCANTERAIT +BROCANTEURS +BROCARDAIENT +BROCARDES +BROCCIO +BROCHAIT +BROCHASSIEZ +BROCHERAI +BROCHERONT +BROCHEUSE +BROCOLI +BRODAS +BRODEES +BRODERAS +BRODEUR +BROIEMENTS +BROIERIONS +BROMATE +BROMHYDRIQUE +BROMOTHYMOL +BRONCHAIENT +BRONCHES +BRONCHIQUE +BRONZAIENT +BRONZASSENT +BRONZER +BRONZERIONS +BRONZEZ +BROQUARD +BROSSAGES +BROSSASSES +BROSSERA +BROSSERIEZ +BROSSIEZ +BROUETTAGES +BROUETTASSES +BROUETTERA +BROUETTERONS +BROUETTONS +BROUILLAIS +BROUILLAT +BROUILLERAI +BROUILLONNA +BROUILLONNAT +BROUT +BROUTARD +BROUTAT +BROUTERAI +BROUTERONT +BROWNIEN +BROYAI +BROYASSIEZ +BROYES +BRU +BRUCHE +BRUGNONIER +BRUINE +BRUIRAI +BRUIRONS +BRUISSAMES +BRUISSES +BRUITAIENT +BRUITASSIONS +BRUITERAIENT +BRUITES +BRUITONS +BRULANT +BRULAT +BRULERAI +BRULERIONS +BRULIONS +BRUMA +BRUMASSERAIT +BRUMEUX +BRUMISASSENT +BRUMISE +BRUMISERAS +BRUMISIONS +BRUNCHAIENT +BRUNCHERAIT +BRUNCHIEZ +BRUNELLES +BRUNIR +BRUNIRIONS +BRUNISSE +BRUNISSIEZ +BRUNOISES +BRUSQUAIS +BRUSQUAT +BRUSQUERONS +BRUTALEMENT +BRUTALISASSE +BRUTALISEE +BRUTALISME +BRUTS +BRUYANTE +BRYONES +BUANDIERES +BUCAILLE +BUCCINATEUR +BUCHAMES +BUCHE +BUCHERAS +BUCHERONNAT +BUCHERONNONS +BUCHEUSES +BUDAPESTOISE +BUDGETAIRES +BUDGETES +BUDGETISANT +BUDGETISIEZ +BUES +BUFFETS +BUFFLAS +BUFFLEES +BUFFLEREZ +BUFFLETIN +BUFONIDES +BUGGANT +BUGGEE +BUGGERENT +BUGGIONS +BUGNAIENT +BUGNASSIONS +BUGNERAIENT +BUGNES +BUIRE +BUISSONNERAI +BUISSONNIEZ +BUKAVIENNES +BULBICULTEUR +BULBULS +BULLAIENT +BULLASSES +BULLEE +BULLERENT +BULLEUSES +BULOTS +BUNKERISA +BUNKERISERA +BUNS +BUQUAS +BUQUEES +BUQUEREZ +BURALISTE +BURELLE +BURGAUX +BURGRAVES +BURINAIS +BURINAT +BURINERAIS +BURINEUR +BURKAS +BURLE +BURNEE +BURSERACEE +BUSAIENT +BUSASSE +BUSEE +BUSERENT +BUSH +BUSINESSMAN +BUSQUAIENT +BUSQUASSIONS +BUSQUERAIENT +BUSQUES +BUSSIEZ +BUTADIENE +BUTANIERS +BUTAT +BUTERAI +BUTERONT +BUTINAGE +BUTINASSENT +BUTINER +BUTINERIONS +BUTINIONS +BUTOMES +BUTTAGES +BUTTASSES +BUTTERA +BUTTERONS +BUTTONS +BUTYREUSES +BUVABLE +BUVEES +BUVEUSES +BUZZA +BUZZASSES +BUZZERA +BUZZERONS +BYLINES +BYZANTIN +CABALAIT +CABALATES +CABALERAIT +CABALEURS +CABALONS +CABANASSE +CABANEMENT +CABANERENT +CABANON +CABASSET +CABIAI +CABINET +CABLAIENT +CABLASSIONS +CABLERA +CABLERIEZ +CABLIER +CABLOTS +CABOCHONS +CABOSSASSENT +CABOSSER +CABOSSERIONS +CABOTAGE +CABOTASSENT +CABOTERAI +CABOTERONT +CABOTINAI +CABOTINERAIS +CABOTINEZ +CABOULOT +CABRANT +CABREE +CABRERAIT +CABRETTES +CABRIOLANT +CABRIOLENT +CABRIOLERIEZ +CABRIOLONS +CACABAI +CACABASSIEZ +CACABERAIS +CACABEZ +CACAO +CACAOUI +CACARDAIT +CACARDATES +CACARDERENT +CACARDONS +CACHAIT +CACHASSIONS +CACHENT +CACHEREZ +CACHETAI +CACHETASSIEZ +CACHETEZ +CACHETONNAIT +CACHETONNE +CACHETS +CACHETTERIEZ +CACHONS +CACHOUS +CACODYLES +CACOLOGIES +CACTACEES +CADASTRA +CADASTRAS +CADASTREE +CADASTRERENT +CADASTRONS +CADDY +CADEAUTANT +CADEAUTEE +CADEAUTERENT +CADEAUTONS +CADENASSAMES +CADENASSE +CADENASSERAS +CADENASSIONS +CADENCASSE +CADENCENT +CADENCERIEZ +CADENES +CADIENNE +CADMIAGES +CADMIASSES +CADMIERA +CADMIERONS +CADOGANS +CADRAMES +CADRAT +CADRENT +CADRERIEZ +CADRIEZ +CADUCITE +CAECALE +CAESALPINIEE +CAFARDAIENT +CAFARDES +CAFARDISES +CAFEIER +CAFES +CAFETAS +CAFETEES +CAFETEREZ +CAFETEUSE +CAFOUILLA +CAFOUILLASSE +CAFOUILLERA +CAFOUILLIONS +CAFTAMES +CAFTAT +CAFTERAIS +CAFTEUR +CAGE +CAGETTE +CAGNE +CAGOTERIES +CAGOULARDES +CAGUAIS +CAGUAT +CAGUERAS +CAGUIONS +CAHOTAIT +CAHOTASSIEZ +CAHOTER +CAHOTERIONS +CAHOTONS +CAIEU +CAILLAIT +CAILLASSAI +CAILLASSERAI +CAILLEBOTTA +CAILLEBOTTAT +CAILLERAIENT +CAILLES +CAILLETAS +CAILLETEAUX +CAILLETTERAI +CAILLETTES +CAILLOUTAGES +CAILLOUTERA +CAILLOUTONS +CAIRN +CAISSIER +CAJEPUT +CAJOLAIT +CAJOLATES +CAJOLERAIT +CAJOLES +CAJOUS +CAKTIS +CALABRIENNES +CALAGES +CALAIT +CALAMENT +CALAMINAIS +CALAMINAT +CALAMINERAIS +CALAMINEZ +CALAMISTRANT +CALAMISTREES +CALAMITES +CALANCHAMES +CALANCHE +CALANCHEREZ +CALANDRA +CALANDRASSE +CALANDRENT +CALANDRERIEZ +CALANDRIEZ +CALASSENT +CALCAIRE +CALCAREUX +CALCEOLAIRE +CALCIFIAIS +CALCIFIAT +CALCIFIERAI +CALCIFIERONT +CALCINA +CALCINASSES +CALCINENT +CALCINERIEZ +CALCIO +CALCITONINE +CALCULAS +CALCULATOIRE +CALCULERAI +CALCULERONT +CALCULONS +CALEBASSES +CALECONNADE +CALEFACTIONS +CALENDES +CALER +CALERIONS +CALETANT +CALETEE +CALETERENT +CALETONS +CALFATAIT +CALFATATES +CALFATERAIT +CALFATIEZ +CALFEUTRAIT +CALFEUTRATES +CALFEUTRES +CALIBRA +CALIBRASSE +CALIBREE +CALIBRERENT +CALIBREUSES +CALICIFORMES +CALIFE +CALINA +CALINASSES +CALINER +CALINERIES +CALINEZ +CALL +CALLASSENT +CALLER +CALLERIONS +CALLIEZ +CALLOVIENNE +CALMAIENT +CALMAS +CALMEES +CALMERENT +CALMIMES +CALMIREZ +CALMISSE +CALMODULINES +CALOMNIAIT +CALOMNIATES +CALOMNIERA +CALOMNIERONS +CALOMNIONS +CALORIFUGEES +CALORIMETRES +CALOTTAIENT +CALOTTES +CALQUAGE +CALQUASSENT +CALQUER +CALQUERIONS +CALQUIONS +CALTAS +CALTEES +CALTEREZ +CALUGE +CALUGEASSENT +CALUGERAI +CALUGERONT +CALVA +CALVAS +CALYCANTHES +CAMAIEUX +CAMARADERIE +CAMARILLA +CAMBA +CAMBALAMES +CAMBALE +CAMBALERAS +CAMBALIONS +CAMBAT +CAMBERAIS +CAMBEZ +CAMBISMES +CAMBOUIS +CAMBOULER +CAMBRAGES +CAMBRASSES +CAMBRENT +CAMBRERIEZ +CAMBREURS +CAMBRIOLAI +CAMBRIOLERAI +CAMBRIONS +CAME +CAMELIDES +CAMELOTS +CAMERAIS +CAMERENT +CAMERLINGAT +CAMESCOPE +CAMIONNAIS +CAMIONNAT +CAMIONNERAIS +CAMIONNETTE +CAMIONS +CAMOUFLA +CAMOUFLASSE +CAMOUFLENT +CAMOUFLERIEZ +CAMOUFLONS +CAMPAGNOLS +CAMPANELLES +CAMPANISTE +CAMPASSE +CAMPEE +CAMPERA +CAMPERONS +CAMPHRAIENT +CAMPHRES +CAMPOS +CANA +CANADIENNE +CANAILLOUS +CANALISABLES +CANALISASSES +CANALISERAIT +CANALISIEZ +CANANT +CANARDAGE +CANARDASSENT +CANARDEES +CANARDEREZ +CANARDIONS +CANAS +CANAT +CANCANAIT +CANCANATES +CANCANERENT +CANCANIERES +CANCELLAIS +CANCELLERAI +CANCELLERONT +CANCEREUX +CANCERISAMES +CANCERISEZ +CANCERS +CANCROIDES +CANDIDA +CANDIDATAS +CANDIDATER +CANDIDATURES +CANDIRA +CANDIRONS +CANDISSENT +CANE +CANEPHORE +CANERAS +CANETIERE +CANGE +CANICULE +CANINE +CANITIE +CANNABIQUES +CANNAIS +CANNAT +CANNELAI +CANNELASSIEZ +CANNELEZ +CANNELLERAIS +CANNELLONIS +CANNERAIT +CANNETAGES +CANNEUSES +CANNIBALISME +CANNISSES +CANOEISTE +CANONICAT +CANONISAI +CANONISER +CANONNA +CANONNANT +CANONNEE +CANONNERENT +CANONNIERES +CANOT +CANOTAMES +CANOTE +CANOTEREZ +CANOTEZ +CANOUNS +CANTALIEN +CANTALS +CANTATE +CANTER +CANTERIONS +CANTHARIDE +CANTINAI +CANTINASSIEZ +CANTINERAI +CANTINERONT +CANTIONS +CANTONAISES +CANTONNASSE +CANTONNEMENT +CANTONNERENT +CANTONNIERES +CANTS +CANULANTS +CANULASSIONS +CANULERAIENT +CANULES +CANYON +CANZONETTE +CAOUAS +CAP +CAPACITATION +CAPAIS +CAPASSENT +CAPEAIS +CAPEAT +CAPEERAS +CAPEIONS +CAPELANS +CAPELE +CAPELINE +CAPELLERAIT +CAPEONS +CAPERIEZ +CAPETIEN +CAPEYAI +CAPEYASSIEZ +CAPEYERAIS +CAPEYEZ +CAPILLARITE +CAPIONS +CAPITALE +CAPITALISANT +CAPITALISE +CAPITALISME +CAPITANE +CAPITEUSE +CAPITONNA +CAPITONNASSE +CAPITONNENT +CAPITOUL +CAPITULAIS +CAPITULARDS +CAPITULERENT +CAPITULONS +CAPONNAI +CAPONNASSIEZ +CAPONNERAIS +CAPONNEZ +CAPORALISA +CAPORALISES +CAPOTA +CAPOTASSE +CAPOTENT +CAPOTERIEZ +CAPOUAN +CAPRES +CAPRICES +CAPRIQUE +CAPRONNIERS +CAPSIDES +CAPSULAIRE +CAPSULASSIEZ +CAPSULERAI +CAPSULERIONS +CAPSULITES +CAPTAIT +CAPTASSIONS +CAPTATIVES +CAPTENT +CAPTERIEZ +CAPTIEUSES +CAPTIVAIT +CAPTIVASSIEZ +CAPTIVERAI +CAPTIVERONT +CAPTURAI +CAPTURASSIEZ +CAPTURERAI +CAPTURERONT +CAPUCHON +CAPUCHONNERA +CAPUCINE +CAPVERDIENS +CAQUAMES +CAQUE +CAQUERAIS +CAQUET +CAQUETANTE +CAQUETATES +CAQUETERAIT +CAQUETEURS +CAQUETTE +CAQUIONS +CARABINEES +CARACALS +CARACOLADES +CARACOLASSES +CARACOLES +CARACTERISES +CARAFA +CARAFASSE +CARAFENT +CARAFERIEZ +CARAIBE +CARAMBOLAIS +CARAMBOLAT +CARAMBOLEZ +CARAMELISAT +CARAMELISES +CARAPACE +CARAPATASSE +CARAPATENT +CARAPATERIEZ +CARAQUES +CARAVANIERE +CARAVELLES +CARBOCHIMIE +CARBONADO +CARBONATERA +CARBONEE +CARBONISAI +CARBONISEES +CARBONISEREZ +CARBONITRURA +CARBONITRURE +CARBORANE +CARBURA +CARBURAS +CARBURATION +CARBURER +CARBURERIONS +CARCAILLA +CARCAILLES +CARCASSES +CARCINOGENE +CARDAI +CARDANS +CARDE +CARDERAS +CARDERONT +CARDIALES +CARDIAUX +CARDINALICES +CARDIOLOGUES +CARDIOTOMIE +CARELIEN +CARENAIENT +CARENASSIONS +CARENCAS +CARENCEES +CARENCEREZ +CARENE +CARENERAS +CARENIONS +CARESSAIT +CARESSASSIEZ +CARESSERAI +CARESSERONT +CARET +CARGUAI +CARGUASSIEZ +CARGUERAI +CARGUERONT +CARIAI +CARIASSE +CARIBEEN +CARICATURAIS +CARICATURERA +CARIE +CARIERAS +CARIEUX +CARILLONNAI +CARILLONNERA +CARILLONS +CARIS +CARLINGUE +CARMAGNOLES +CARMINA +CARMINASSES +CARMINEE +CARMINERENT +CARMINONS +CARNAU +CARNEAUX +CARNIEN +CARNIFIAIT +CARNIFIATES +CARNIFIES +CARNOTSETS +CAROLOS +CAROTENOIDE +CAROTTAGES +CAROTTASSES +CAROTTERA +CAROTTERONS +CAROTTIERES +CAROUGES +CARPELLAIRE +CARPETTIERS +CARPOCAPSES +CARRAIENT +CARRASSES +CARREAUTES +CARRELAIENT +CARRELETS +CARRELLERA +CARRELLERONT +CARRERAIT +CARRICK +CARRIEZ +CARROIERAIT +CARRON +CARROSSAIT +CARROSSATES +CARROSSERAIT +CARROSSES +CARROYAGE +CARROYASSENT +CARROYERENT +CARS +CARTAIERAIS +CARTAIT +CARTATES +CARTAYASSENT +CARTAYERAI +CARTAYERONT +CARTELETTE +CARTELLISEES +CARTENT +CARTERIE +CARTESIEN +CARTHAME +CARTIONS +CARTONNA +CARTONNASSE +CARTONNENT +CARTONNERIE +CARTONNEUSES +CARTONS +CARTOPHILIES +CARTOUCHAMES +CARVA +CARYOCINESES +CARYOPHYLLEE +CASABLANCAIS +CASAMES +CASAQUIN +CASBAH +CASCADANT +CASCADENT +CASCADERIEZ +CASCADIEZ +CASE +CASEIFIAIENT +CASEIFIERA +CASEIFIERONS +CASEMATAI +CASEMATERAI +CASEMATERONT +CASERAIENT +CASERIEZ +CASERNASSE +CASERNEMENT +CASERNERENT +CASERNIEZ +CASEZ +CASILLEUSES +CASINOTIERES +CASQUAIT +CASQUATES +CASQUERAIT +CASQUETTERIE +CASQUONS +CASSAMES +CASSASSES +CASSE +CASSERAI +CASSERIONS +CASSETTE +CASSIER +CASSISSIER +CASSOT +CASTAGNAIS +CASTAGNAT +CASTAGNERAIS +CASTAGNETTES +CASTANT +CASTASSES +CASTELETS +CASTERAI +CASTERONT +CASTILLANES +CASTOR +CASTRAIENT +CASTRASSE +CASTRATIONS +CASTRERAI +CASTRERONT +CASTRUM +CASUISTE +CATABOLISMES +CATACLYSMES +CATADROMES +CATALANE +CATALOGUAMES +CATALOGUE +CATALOGUERAS +CATALOGUEZ +CATALYSAIT +CATALYSATES +CATALYSERAIT +CATALYSEURS +CATAMENIALE +CATAPLASMES +CATAPULTAIS +CATAPULTAT +CATAPULTEZ +CATARRHALES +CATASTASE +CATASTROPHEZ +CATATONIE +CATCHAMES +CATCHE +CATCHEREZ +CATCHEZ +CATECHETIQUE +CATECHISANT +CATECHISIEZ +CATEGORISA +CATEGORISEES +CATELLES +CATHARE +CATHEDRAUX +CATHETERISAI +CATHETERISME +CATHODIQUE +CATHOLICITES +CATILINAIRES +CATINAIT +CATINATES +CATINERAIT +CATINIEZ +CATIRAI +CATIRONT +CATISSES +CATITES +CAUCASIEN +CAUDALES +CAUDINES +CAULICOLES +CAUSAL +CAUSAMES +CAUSASSIONS +CAUSEES +CAUSEREZ +CAUSEUR +CAUSSENARDE +CAUSTIQUES +CAUTERISAI +CAUTERISER +CAUTIONNAI +CAUTIONNER +CAVAGE +CAVALAIENT +CAVALASSIONS +CAVALCADAS +CAVALCADER +CAVALE +CAVALERAS +CAVALEUR +CAVALIEZ +CAVASSIONS +CAVEE +CAVERENT +CAVERNICOLES +CAVIARDAGES +CAVIARDASSES +CAVIARDERA +CAVIARDERONS +CAVICORNE +CAVITE +CAYENNE +CEANS +CEBUANOS +CEDA +CEDAS +CEDEES +CEDEREZ +CEDEX +CEDRAIES +CEDULAIRES +CEGESIMALE +CEIGNENT +CEIGNISSES +CEINDRAIT +CEINTE +CEINTURAMES +CEINTURE +CEINTURERAS +CEINTURIONS +CELAMES +CELAT +CELEBRANTS +CELEBRATIONS +CELEBRERAIT +CELEBREZ +CELENT +CELERI +CELES +CELIBAT +CELINIENNE +CELLAS +CELLIERS +CELLULASES +CELLULOID +CELTE +CELTISME +CEMENTA +CEMENTASSES +CEMENTENT +CEMENTERIEZ +CEMENTIONS +CENDRAIS +CENDRAT +CENDRERAIS +CENDREUSE +CENDRONS +CENOBITISMES +CENSE +CENSIERE +CENSORIALE +CENSURAI +CENSURASSIEZ +CENSURERAI +CENSURERONT +CENTAUREE +CENTENNALE +CENTIBARS +CENTILE +CENTON +CENTRAIENT +CENTRALISA +CENTRALISEE +CENTRALISMES +CENTRANT +CENTRATES +CENTRERAI +CENTRERONT +CENTRIFUGEAI +CENTRISME +CENTROSOMES +CENTUPLAIENT +CENTUPLES +CENTURIONS +CEPES +CERAISTES +CERAMISTE +CERATOPSIENS +CERCEAUX +CERCLAIT +CERCLATES +CERCLERAIT +CERCLIEZ +CERDAGNOLE +CEREBRAL +CEREMONIAUX +CEREUSE +CERISETTES +CERMETS +CERNAS +CERNEAUX +CERNERAS +CERNIONS +CERTAINE +CERTIFIAIT +CERTIFIER +CERTITUDES +CERUMENS +CERVAISON +CERVICALE +CERVIDES +CESARIENS +CESARISER +CESARS +CESSAIT +CESSASSIEZ +CESSER +CESSERIONS +CESSION +CESTODOSES +CETEAU +CETOINE +CETONIQUE +CEVADILLE +CEYLANAISES +CHABANOUS +CHABLAIS +CHABLAT +CHABLERAIS +CHABLEZ +CHABROL +CHACONNE +CHAENICHTHYS +CHAFOUINES +CHAGRINANTE +CHAGRINATES +CHAGRINERAIT +CHAGRINIEZ +CHAHUTAI +CHAHUTASSIEZ +CHAHUTERAI +CHAHUTERONT +CHAHUTS +CHAINAIT +CHAINASSES +CHAINERA +CHAINERONS +CHAINEURS +CHAINON +CHAIS +CHALANDAGE +CHALAZIONS +CHALCOPYRITE +CHALDEEN +CHALLENGEAI +CHALLENGERS +CHALONE +CHALOUPAMES +CHALOUPE +CHALOUPERAS +CHALOUPIERS +CHALUTAI +CHALUTASSIEZ +CHALUTERAIS +CHALUTEZ +CHAMAILLA +CHAMAILLERA +CHAMAILLIEZ +CHAMANISTE +CHAMARRAS +CHAMARREES +CHAMARREREZ +CHAMARRURE +CHAMBARDAS +CHAMBARDEES +CHAMBARDERAS +CHAMBARDIONS +CHAMBERIENS +CHAMBOULAS +CHAMBOULEES +CHAMBOULERAS +CHAMBOULIONS +CHAMBRANLEZ +CHAMBRERA +CHAMBRERONS +CHAMBRIER +CHAMEAUX +CHAMITIQUE +CHAMOISAMES +CHAMOISE +CHAMOISERAS +CHAMOISETTE +CHAMOISIONS +CHAMPAGNES +CHAMPAGNISEZ +CHAMPENOISES +CHAMPIGNONS +CHAMPLEURE +CHAMPLEVASSE +CHAMPLEVENT +CHAMPLURES +CHANCEL +CHANCELANTS +CHANCELER +CHANCELLENT +CHANCEUX +CHANCIRAIT +CHANCISSAIS +CHANCISSURE +CHANCREUX +CHANDELLES +CHANFREINER +CHANGEABLE +CHANGEANTS +CHANGEES +CHANGERAIT +CHANGEURS +CHANNE +CHANSONNA +CHANSONNERA +CHANSONNIEZ +CHANTAI +CHANTASSE +CHANTEE +CHANTERAIENT +CHANTERONS +CHANTIEZ +CHANTONNA +CHANTONNENT +CHANTOUNG +CHANTRERIE +CHAOS +CHAOUIS +CHAPARDAMES +CHAPARDE +CHAPARDERAS +CHAPARDEUSE +CHAPEAUTA +CHAPEAUTASSE +CHAPEAUTENT +CHAPEE +CHAPELANT +CHAPELEE +CHAPELIERES +CHAPELLERONT +CHAPERONNAIT +CHAPERONNE +CHAPERONNONS +CHAPITRAL +CHAPITRERAI +CHAPITRERONT +CHAPO +CHAPONNANT +CHAPONNEAU +CHAPONNERAIT +CHAPONNEURS +CHAPSKAS +CHAPTALISENT +CHARABIA +CHARANCON +CHARBONNAGE +CHARBONNER +CHARBONNEUX +CHARCUTA +CHARCUTANT +CHARCUTEE +CHARCUTERENT +CHARCUTIER +CHARDONNA +CHARDONNENT +CHARDONNONS +CHARGEAIT +CHARGEATES +CHARGERAIENT +CHARGES +CHARIA +CHARIOTAMES +CHARIOTE +CHARIOTERAS +CHARIOTIONS +CHARITE +CHARLOTTE +CHARMANTES +CHARME +CHARMERAS +CHARMEUSE +CHARNURE +CHAROGNES +CHARPENTONS +CHARRETIER +CHARRETTES +CHARRIAMES +CHARRIE +CHARRIERAS +CHARRIEUSE +CHARRIOTAI +CHARRIOTERAI +CHARROIERA +CHARROIERONT +CHARROYAI +CHARROYEUR +CHARRUAIENT +CHARRUES +CHARTERISAT +CHARTERISIEZ +CHARTISTES +CHARTRIER +CHASSAI +CHASSASSE +CHASSEENNE +CHASSERAI +CHASSERIONS +CHASSIES +CHASTE +CHASUBLIERE +CHATAIGNANT +CHATAIGNEE +CHATAIGNIER +CHATAIRES +CHATELAINS +CHATIAIS +CHATIAT +CHATIERAIS +CHATIERONT +CHATOIEMENTS +CHATOIERIONS +CHATONNAMES +CHATONNE +CHATONNEREZ +CHATONS +CHATOUILLEE +CHATOUILLEUX +CHATOYAIS +CHATOYASSES +CHATOYIONS +CHATRASSE +CHATRENT +CHATRERIEZ +CHATRIEZ +CHATTAIS +CHATTAT +CHATTERAIS +CHATTERONT +CHATTIENNE +CHAUDEAU +CHAUDIN +CHAUFFAI +CHAUFFARDS +CHAUFFEE +CHAUFFERENT +CHAUFFES +CHAUFFONS +CHAULAIS +CHAULAT +CHAULERAIS +CHAULEUSE +CHAUMAGES +CHAUMASSE +CHAUMENT +CHAUMERIEZ +CHAUMIEZ +CHAUSSAIENT +CHAUSSASSENT +CHAUSSER +CHAUSSERIONS +CHAUSSIONS +CHAUVAIT +CHAUVINE +CHAUVIRAI +CHAUVIRONT +CHAUX +CHAVIRAIS +CHAVIRAT +CHAVIRERAI +CHAVIRERONT +CHAYOTTE +CHEBS +CHEDDITES +CHEFFE +CHEIKH +CHELATAI +CHELATASSIEZ +CHELATERAI +CHELATERONT +CHELEMS +CHELLEENNE +CHEMINAIENT +CHEMINENT +CHEMINERIEZ +CHEMINOTE +CHEMISAIT +CHEMISATES +CHEMISERAIT +CHEMISES +CHEMISONS +CHENAIES +CHENALASSE +CHENALERA +CHENALERONS +CHENAUX +CHENEVOTTE +CHENILS +CHEQUE +CHERAMES +CHERASSIONS +CHERCHAS +CHERCHEES +CHERCHEREZ +CHERCHEZ +CHERERAIENT +CHERES +CHERIFATS +CHERIMOLIERS +CHERIREZ +CHERISSAIT +CHERITES +CHERRAIS +CHERRAT +CHERRERAS +CHERRIEZ +CHERVIS +CHETIVITE +CHEVAIENT +CHEVALAIT +CHEVALATES +CHEVALERIEZ +CHEVALIERES +CHEVANT +CHEVAUCHAI +CHEVAUCHASSE +CHEVAUCHONS +CHEVELUE +CHEVERAI +CHEVERONT +CHEVEZ +CHEVILLANT +CHEVILLATES +CHEVILLERAIT +CHEVILLES +CHEVILLONS +CHEVRETANT +CHEVRETER +CHEVRETTAIS +CHEVRETTAT +CHEVRETTERAS +CHEVRETTIONS +CHEVRON +CHEVRONNAS +CHEVRONNEES +CHEVRONNEREZ +CHEVRONS +CHEVROTAMES +CHEVROTERA +CHEVROTERONS +CHEVROTONS +CHIADAIS +CHIADAT +CHIADERAIS +CHIADEUR +CHIAIS +CHIALASSE +CHIALERA +CHIALERONS +CHIALONS +CHIAS +CHIASSIEZ +CHIBRAMES +CHIBRE +CHIBREREZ +CHIC +CHICANASSIEZ +CHICANERAI +CHICANERIONS +CHICANIERE +CHICHES +CHICLE +CHICOS +CHICOTASSE +CHICOTENT +CHICOTERIEZ +CHICOTONS +CHICOTTASSE +CHICOTTENT +CHICOTTERIEZ +CHICOUANGUES +CHIENDENT +CHIER +CHIERIES +CHIEZ +CHIFFONNERA +CHIFFONNONS +CHIFFRAIT +CHIFFRATES +CHIFFRES +CHIFFRONS +CHIGNASSENT +CHIGNERAI +CHIGNERONT +CHIHUAHUAS +CHIKWANGUES +CHILOMS +CHIMIO +CHIMISTE +CHINAIENT +CHINASSIONS +CHINDAIS +CHINDAT +CHINDERAS +CHINDIONS +CHINERAIS +CHINEUR +CHINOISAIENT +CHINOISERAIT +CHINOISES +CHINURE +CHIP +CHIPAS +CHIPEES +CHIPEREZ +CHIPEUSES +CHIPOLATA +CHIPOTAMES +CHIPOTE +CHIPOTERAS +CHIPOTEUR +CHIPPEWAS +CHIQUASSE +CHIQUEMENT +CHIQUERAS +CHIQUEUSE +CHIRALES +CHIROGRAPHES +CHIROMANCIEN +CHIROPTERES +CHIRURGIEN +CHISINOVIENS +CHIURE +CHLEUHES +CHLINGUASSE +CHLINGUERA +CHLINGUERONS +CHLORAL +CHLORASSIONS +CHLOREES +CHLORERAIS +CHLOREUSE +CHLORIQUE +CHLOROFORME +CHLOROMETRIE +CHLOROQUINES +CHLORURAIS +CHLORURAT +CHLORURERAI +CHLORURERONT +CHOANE +CHOCOLATEE +CHOCOLATINES +CHOFARS +CHOIERIEZ +CHOIRAIT +CHOISIES +CHOISIREZ +CHOISISSE +CHOIX +CHOLEDOQUES +CHOLERINES +CHOLINES +CHOMABLE +CHOMAS +CHOMEDUS +CHOMERAS +CHOMEUSE +CHONDROSTEEN +CHOPAMES +CHOPE +CHOPERAS +CHOPIN +CHOPINASSENT +CHOPINERAI +CHOPINERONT +CHOPPAI +CHOPPASSIEZ +CHOPPERAIS +CHOPPES +CHOQUAIT +CHOQUASSENT +CHOQUER +CHOQUERIONS +CHORALES +CHOREES +CHORIALE +CHORISTES +CHOROIDIENNE +CHORTENS +CHOSIFIANT +CHOSIFIERAIT +CHOSIFIIEZ +CHOUANNAI +CHOUANNERAIS +CHOUANNERONT +CHOUCHEN +CHOUCHOUTAIT +CHOUCHOUTE +CHOUCHOUTONS +CHOUGNAIT +CHOUGNATES +CHOUGNERENT +CHOUGNONS +CHOUINAS +CHOUINER +CHOUINERIONS +CHOUPETTE +CHOURAIENT +CHOURASSIONS +CHOURAVAS +CHOURAVEES +CHOURAVEREZ +CHOURE +CHOURERAS +CHOURIN +CHOURINER +CHOURINS +CHOYAIENT +CHOYASSIONS +CHOYIEZ +CHRISME +CHRISTS +CHROMAS +CHROMATIDES +CHROMATISAIT +CHROMATISE +CHROMATISME +CHROMATOPSIE +CHROMERAIS +CHROMEUR +CHROMIQUE +CHROMISASSE +CHROMISEE +CHROMISERENT +CHROMISONS +CHRONICISAT +CHRONICISIEZ +CHRONIQUAMES +CHRONIQUE +CHRONIQUEURS +CHRONOGRAMME +CHRONOMETRAS +CHRONOMETRES +CHRYSOMELES +CHRYSOPRASES +CHTHONIENNE +CHTONIENS +CHUCHOTANT +CHUCHOTEE +CHUCHOTERAIT +CHUCHOTES +CHUE +CHUINTANTES +CHUINTE +CHUINTERAIS +CHUINTEZ +CHUTAMES +CHUTE +CHUTERAS +CHUTEZ +CHYLEUSE +CHYMOSINES +CIAO +CIBLAI +CIBLASSIEZ +CIBLERAI +CIBLERONT +CIBOULE +CICATRICE +CICATRISAS +CICATRISE +CICATRISERAS +CICATRISIONS +CICHLIDE +CICLANT +CICLEE +CICLERENT +CICLONS +CIDRERIE +CIELS +CIGARES +CIGARIERS +CIGUES +CILIEES +CILLASSE +CILLEMENT +CILLERENT +CILLONS +CIMENTAIENT +CIMENTASSES +CIMENTENT +CIMENTERIE +CIMENTIERES +CIMETIERES +CINABRES +CINEASTE +CINEMASCOPE +CINEMOGRAPHE +CINEPHILIE +CINEROMAN +CINGALAISE +CINGLAIS +CINGLASSES +CINGLERA +CINGLERONS +CINNAMOMES +CINSAUT +CINTRANT +CINTREE +CINTRERENT +CINTRIEZ +CIPAYES +CIRAGE +CIRASSENT +CIRCAETE +CIRCONCIMES +CIRCONCIRIEZ +CIRCONCISENT +CIRCONFLEXES +CIRCONSPECTS +CIRCONVENIR +CIRCONVINMES +CIRCONVOISIN +CIRCUIT +CIRCULAIS +CIRCULARISEE +CIRCULARITES +CIRCULATEURS +CIRCULES +CIRERAIENT +CIRES +CIRIERS +CIRQUES +CIRRIPEDES +CISAILLAI +CISAILLER +CISALPINES +CISELAIT +CISELATES +CISELERAIENT +CISELES +CISELLEMENT +CISJURANE +CISPADANES +CISTACEE +CISTICOLIDE +CITA +CITAIT +CITATES +CITENT +CITERIEUR +CITERONS +CITIEZ +CITRATES +CITRONNADES +CITRONNASSES +CITRONNENT +CITRONNERIEZ +CITRONNONS +CIVAITES +CIVIERES +CIVILISAIS +CIVILISAT +CIVILISEE +CIVILISERENT +CIVILISONS +CIVISMES +CLABAUDANT +CLABAUDENT +CLABAUDERIE +CLABAUDEUSES +CLABOTAI +CLABOTASSIEZ +CLABOTERAI +CLABOTERONT +CLADOGRAMME +CLAIRANCE +CLAIRETTES +CLAIRONNANT +CLAIRONNAT +CLAIRONNEZ +CLAIRSEMAIT +CLAIRSEMATES +CLAIRSEMIEZ +CLAIRVOYANTS +CLAMASSE +CLAMECA +CLAMECASSES +CLAMECERA +CLAMECERONS +CLAMER +CLAMERIONS +CLAMP +CLAMPAS +CLAMPEES +CLAMPEREZ +CLAMPIONS +CLAMPSAS +CLAMPSEES +CLAMPSEREZ +CLAMS +CLAMSASSENT +CLAMSER +CLAMSERIONS +CLANDES +CLANIQUE +CLAPI +CLAPIRAIS +CLAPISSAIENT +CLAPISSONS +CLAPOTAIENT +CLAPOTASSENT +CLAPOTER +CLAPOTERIONS +CLAPOTIS +CLAPPAS +CLAPPEMENTS +CLAPPEREZ +CLAPS +CLAQUANTE +CLAQUATES +CLAQUEMURAT +CLAQUEMURIEZ +CLAQUERAS +CLAQUETAI +CLAQUETONS +CLAQUETTEREZ +CLAQUIONS +CLARIFIAIS +CLARIFIASSES +CLARIFIENT +CLARIFIERIEZ +CLARINES +CLARTES +CLASSAIENT +CLASSASSE +CLASSEMENT +CLASSERENT +CLASSEUSES +CLASSIFIA +CLASSIFIIEZ +CLATHRATE +CLAUDICANTES +CLAUDIQUANT +CLAUDIQUENT +CLAUSES +CLAUSTRANT +CLAUSTRERAIS +CLAUSTREZ +CLAVAIRES +CLAVARDAIS +CLAVARDAT +CLAVARDERAS +CLAVARDIONS +CLAVE +CLAVELEE +CLAVERA +CLAVERONS +CLAVETAMES +CLAVETE +CLAVETTE +CLAVISTES +CLAYONNA +CLAYONNASSE +CLAYONNENT +CLAYONNERIEZ +CLAYS +CLEBARDS +CLEMENT +CLEMENVILLA +CLENCHASSE +CLENCHENT +CLENCHERIEZ +CLENCHONS +CLEPTOMANIES +CLERGYMANS +CLERICATURES +CLEROUQUIES +CLICHAMES +CLICHE +CLICHERAS +CLICHEUR +CLICS +CLIENTS +CLIGNASSENT +CLIGNEMENTS +CLIGNEREZ +CLIGNOTA +CLIGNOTAS +CLIGNOTEREZ +CLIGNOTIONS +CLIMATISANT +CLIMATISEURS +CLINAMEN +CLINICIENNES +CLIPPAI +CLIPPASSIEZ +CLIPPERAI +CLIPPERONT +CLIPSAIENT +CLIPSASSIONS +CLIPSERAIENT +CLIPSES +CLIQUAIS +CLIQUASSIEZ +CLIQUERAI +CLIQUERONT +CLIQUETANTE +CLIQUETATES +CLIQUETONS +CLIQUIONS +CLISSANT +CLISSEE +CLISSERENT +CLISSONS +CLITORISME +CLIVAIS +CLIVAT +CLIVERAIS +CLIVEZ +CLOAQUE +CLOCHARDE +CLOCHARDISES +CLOCHASSIEZ +CLOCHERAI +CLOCHERONT +CLOCHONS +CLOISONNERA +CLOITRA +CLOITRASSES +CLOITRERA +CLOITRERONS +CLONAGE +CLONANT +CLONE +CLONERAS +CLONIES +CLOPAIS +CLOPAT +CLOPERAIS +CLOPET +CLOPINANT +CLOPINENT +CLOPINERIEZ +CLOPIONS +CLOPPANT +CLOPPENT +CLOPPERIEZ +CLOQUAGE +CLOQUASSENT +CLOQUER +CLOQUERIONS +CLORAIENT +CLOS +CLOSIEZ +CLOTURAIENT +CLOTURES +CLOUAIENT +CLOUASSIONS +CLOUERA +CLOUERONS +CLOUPS +CLOUTAIT +CLOUTASSIONS +CLOUTERAIENT +CLOUTERONS +CLOUTONS +CLOWNS +CLUBBEUR +CLUNISIENNE +CLUSIACEES +CNIDOBLASTES +COACERVATS +COACHASSE +COACHENT +COACHERIEZ +COACHONS +COACTIVE +COAGULAIENT +COAGULASSENT +COAGULATRICE +COAGULERAIS +COAGULEZ +COALESCAIT +COALESCATES +COALESCER +COALISAIENT +COALISES +COAPTATIONS +COASSAIS +COASSAT +COASSERAIS +COASSEZ +COASSURAIENT +COASSURASSES +COASSURERA +COASSURERONS +COATIS +COBAEA +COBAYE +COBIER +COCAGNE +COCAINOMANES +COCARDIERS +COCCIDIES +COCHAIENT +COCHASSIONS +COCHENT +COCHERES +COCHETTE +COCHLEAIRES +COCHONNAIENT +COCHONNASSES +COCHONNERA +COCHONNERIEZ +COCHONNONS +COCKTAILS +COCOLASSE +COCOLENT +COCOLERIEZ +COCONISATION +COCONNANT +COCONNENT +COCONNERIEZ +COCONNONS +COCOONAIS +COCOONAT +COCOONERAIS +COCOONEZ +COCOTA +COCOTASSES +COCOTERAIE +COCOTERONS +COCOTTAI +COCOTTASSIEZ +COCOTTERAIS +COCOTTEZ +COCU +COCUFIAIS +COCUFIAT +COCUFIERAIS +COCUFIEZ +CODAI +CODASSE +CODEBITRICE +CODECIDAS +CODECIDEES +CODECIDEREZ +CODECISION +CODERAS +CODETENAIT +CODETIENDRAS +CODETIENT +CODETINTES +CODICILLE +CODIFIAI +CODIFIASSIEZ +CODIFIE +CODIFIERAS +CODIFIIONS +CODIRIGEA +CODIRIGERA +CODIRIGERONS +CODOMINANTES +COECHANGISTE +COEDITASSE +COEDITENT +COEDITERIEZ +COEDITIONS +COELACANTHE +COEMPTION +COEPOUSE +COERCIBLE +COETERNELLES +COEXISTAIT +COEXISTATES +COEXISTERAIT +COEXISTIEZ +COFFINS +COFFRAS +COFFREES +COFFREREZ +COFFREZ +COFINANCAIT +COFINANCATES +COFINANCES +COGERANTE +COGERATES +COGERERAIT +COGERIEZ +COGITAIS +COGITAT +COGITERAI +COGITERONT +COGNACS +COGNASSES +COGNATIQUES +COGNERAI +COGNERONT +COHABITAIT +COHABITERAI +COHABITERONT +COHERENTES +COHERITANT +COHERITENT +COHERITERIEZ +COHERITIEZ +COHESIVES +COHOBASSENT +COHOBEES +COHOBEREZ +COHORTE +COIFFAIS +COIFFASSES +COIFFERA +COIFFERONS +COIFFONS +COINCAIT +COINCATES +COINCERAIENT +COINCES +COINCHANT +COINCHEE +COINCHERENT +COINCHONS +COINCIDENTE +COINCIDERENT +COINCIDONS +COINS +COITAMES +COITE +COITEREZ +COITRON +COKAGES +COKEFIAIT +COKEFIASSIEZ +COKEFIERAI +COKEFIERONT +COKEURS +COLATURE +COLCHIQUE +COLEE +COLEREUSE +COLICITANTES +COLINEAIRES +COLIS +COLISAS +COLISEES +COLISEREZ +COLISTIER +COLITIGANTES +COLLABORANT +COLLABORENT +COLLAGE +COLLAIS +COLLAPSAIS +COLLAPSAT +COLLAPSERAS +COLLAPSIONS +COLLASSIONS +COLLATEUR +COLLATIONS +COLLECTAIT +COLLECTATES +COLLECTERAIT +COLLECTEURS +COLLECTIVISA +COLLECTIVISE +COLLECTONS +COLLEGIALE +COLLEGUE +COLLERAIENT +COLLERONS +COLLETAILLEE +COLLETAIT +COLLETATES +COLLETIEZ +COLLEUR +COLLIGE +COLLIGER +COLLIGERIONS +COLLIMATIONS +COLLOCATION +COLLONS +COLLOQUER +COLLUSION +COLLYRE +COLMATAIENT +COLMATES +COLOMBAGE +COLOMBIENNES +COLOMBITE +COLON +COLONIAL +COLONISA +COLONISASSE +COLONISES +COLONNETTES +COLOQUINTE +COLORANT +COLORAT +COLORECTALE +COLORERAIS +COLOREZ +COLORIAS +COLORIEES +COLORIEREZ +COLORIMETRES +COLORISAIS +COLORISAT +COLORISERAI +COLORISERONT +COLOSCOPE +COLOSSES +COLPORTAS +COLPORTEES +COLPORTEREZ +COLPORTEZ +COLTAN +COLTINAMES +COLTINE +COLTINERAS +COLTINEZ +COLUMBIDE +COLVERT +COMANCHE +COMATAIS +COMATAT +COMATERAS +COMATEUX +COMBATIF +COMBATTANT +COMBATTIFS +COMBATTIT +COMBATTRAIS +COMBATTUE +COMBINAMES +COMBINASSIEZ +COMBINEE +COMBINERENT +COMBINIEZ +COMBLAIT +COMBLASSIONS +COMBLERA +COMBLERONS +COMBRIERES +COMBUSTIONS +COMEDONS +COMETIQUES +COMIQUES +COMITATIVE +COMITIAUX +COMMANDAMES +COMMANDERA +COMMANDERIEZ +COMMANDIEZ +COMMANDITANT +COMMANDITEES +COMMANDONS +COMMEMORAT +COMMEMOREES +COMMEMOREREZ +COMMENCA +COMMENCAS +COMMENCEES +COMMENCERAS +COMMENCIONS +COMMENTAIS +COMMENTAT +COMMENTER +COMMERAGES +COMMERASSES +COMMERCAMES +COMMERCERAIT +COMMERCIAL +COMMERERA +COMMERERONS +COMMETTAGES +COMMETTES +COMMETTRAS +COMMINUTIF +COMMISSAIRE +COMMISSIONNE +COMMISSURAUX +COMMODE +COMMOTIONNE +COMMOTIONS +COMMUAS +COMMUEES +COMMUEREZ +COMMUNAL +COMMUNALISAS +COMMUNALISER +COMMUNARDES +COMMUNES +COMMUNIANTS +COMMUNICABLE +COMMUNIER +COMMUNIQUAI +COMMUNIQUEZ +COMMUNISTE +COMMUTAIT +COMMUTATES +COMMUTES +COMPACITE +COMPACTAIS +COMPACTAT +COMPACTERAIS +COMPACTEUR +COMPAGNIES +COMPARABLE +COMPARAITRAI +COMPARASSIEZ +COMPARATISTE +COMPARENT +COMPARERIEZ +COMPARSE +COMPASSAIT +COMPASSATES +COMPASSES +COMPATIRAIT +COMPATISSAIS +COMPATISSIEZ +COMPENDIUM +COMPENETRONS +COMPENSAS +COMPENSATIF +COMPENSE +COMPENSERAS +COMPENSIONS +COMPETAIT +COMPETATES +COMPETERAI +COMPETERONT +COMPETIS +COMPILAIT +COMPILATES +COMPILENT +COMPILERIEZ +COMPISSA +COMPISSASSES +COMPISSERA +COMPISSERONS +COMPLAIRAI +COMPLAIRONT +COMPLANT +COMPLANTER +COMPLEMENTAI +COMPLEMENTES +COMPLETAIS +COMPLETAT +COMPLETERAI +COMPLETERONT +COMPLETONS +COMPLEXANT +COMPLEXAT +COMPLEXERAIS +COMPLEXEZ +COMPLEXIFIEZ +COMPLICATION +COMPLIMENTE +COMPLIMENTEZ +COMPLIQUAMES +COMPLIQUE +COMPLIQUERAS +COMPLIQUIONS +COMPLOTAS +COMPLOTEES +COMPLOTEREZ +COMPLOTEZ +COMPLUSSENT +COMPONEES +COMPORTAIS +COMPORTAT +COMPORTENT +COMPORTERIEZ +COMPOSA +COMPOSANTES +COMPOSE +COMPOSERAS +COMPOSEUSE +COMPOSITIONS +COMPOSTAI +COMPOSTERAI +COMPOSTERONT +COMPOTAI +COMPOTASSIEZ +COMPOTERAI +COMPOTERONT +COMPOUNDAGE +COMPOUNDER +COMPRADORE +COMPRENDRAIT +COMPRENETTES +COMPRESSES +COMPRESSION +COMPRIMAIS +COMPRIMAT +COMPRIMERAIS +COMPRIMEZ +COMPRISSES +COMPTAIENT +COMPTASSIEZ +COMPTERAI +COMPTERONT +COMPTIONS +COMPULSANT +COMPULSERAIT +COMPULSIEZ +COMPULSIVES +COMPUTEURS +COMTALE +COMTOISE +CONASSES +CONCASSAGES +CONCASSASSES +CONCASSERA +CONCASSERONS +CONCATENAI +CONCATENER +CONCAVITE +CONCEDASSE +CONCEDENT +CONCEDERIEZ +CONCELEBRAI +CONCELEBRERA +CONCENTRAIT +CONCENTRATES +CONCENTRERA +CONCEPTACLE +CONCEPTRICE +CONCERNAIENT +CONCERNES +CONCERTAIT +CONCERTER +CONCERTINO +CONCESSIBLES +CONCEVABLE +CONCEVONS +CONCEVRONS +CONCHIAIT +CONCHIATES +CONCHIERAIT +CONCHIIEZ +CONCIERGE +CONCILIANTES +CONCILIATEUR +CONCILIEES +CONCILIEREZ +CONCIS +CONCITOYENS +CONCLUANTE +CONCLUONS +CONCLURIEZ +CONCLUSIVES +CONCOCTAIENT +CONCOCTES +CONCOMBRE +CONCORDAI +CONCORDANTS +CONCORDATES +CONCORDERAS +CONCORDIONS +CONCOURENT +CONCOURRAI +CONCOURS +CONCRETA +CONCRETASSES +CONCRETER +CONCRETISEES +CONCRETS +CONCUBINES +CONDAMNAIT +CONDAMNATES +CONDAMNEE +CONDAMNERENT +CONDAMNONS +CONDENSANT +CONDENSERAI +CONDENSERONT +CONDESCENDIT +CONDISCIPLE +CONDOMINIUMS +CONDRUSIEN +CONDUCTIBLE +CONDUIRA +CONDUIRONS +CONDUISEZ +CONDYLIEN +CONFECTION +CONFEDERAI +CONFEDERATES +CONFEDERERAI +CONFERAIS +CONFERAT +CONFEREREZ +CONFERVE +CONFESSASSE +CONFESSENT +CONFESSERIEZ +CONFETTIS +CONFIANTE +CONFIATES +CONFIERAIENT +CONFIES +CONFIGURASSE +CONFIGUREE +CONFIGURONS +CONFINANT +CONFINEE +CONFINERAIT +CONFINIEZ +CONFIRAIS +CONFIRMAIENT +CONFIRMASSE +CONFIRMES +CONFISAIT +CONFISERIE +CONFISONS +CONFISQUER +CONFISSES +CONFLICTUEL +CONFLUAIT +CONFLUATES +CONFLUERAI +CONFLUERONT +CONFONDANT +CONFONDIONS +CONFONDONS +CONFONDRIONS +CONFORMAIENT +CONFORMEES +CONFORMERENT +CONFORMISME +CONFORTER +CONFORTS +CONFRERIES +CONFRONTAS +CONFRONTE +CONFRONTERAS +CONFRONTIONS +CONFUS +CONGEABLES +CONGEDIAS +CONGEDIEES +CONGEDIERAS +CONGEDIIONS +CONGELANT +CONGELATEURS +CONGELES +CONGENITALE +CONGESTIONNA +CONGESTIONNE +CONGLOMERAI +CONGLOMERE +CONGLOMERONS +CONGLUTINES +CONGRATULAT +CONGRATULES +CONGREAIT +CONGREATES +CONGREERAIT +CONGRESSISTE +CONGRUES +CONICINES +CONIROSTRES +CONJECTURANT +CONJECTUREE +CONJOIGNIONS +CONJOIGNONS +CONJONCTEURS +CONJONCTIVES +CONJUGALITES +CONJUGUANT +CONJUGUEE +CONJUGUERENT +CONJUGUONS +CONJURAS +CONJURATION +CONJURER +CONJURERIONS +CONNAISSENT +CONNAIT +CONNAITRIONS +CONNEAU +CONNECTAIT +CONNECTATES +CONNECTERAIT +CONNECTEURS +CONNECTIQUE +CONNES +CONNIVENTS +CONNOTASSENT +CONNOTATIVE +CONNOTERAIS +CONNOTEZ +CONNUSSE +CONOPEES +CONQUERIEZ +CONQUERREZ +CONQUIERE +CONQUISSE +CONS +CONSACRASSE +CONSACRENT +CONSACRERIEZ +CONSANGUINE +CONSECUTIONS +CONSEILLAIS +CONSEILLAT +CONSEILLONS +CONSENTAIS +CONSENTES +CONSENTIRAI +CONSENTIRONT +CONSERVAIS +CONSERVAT +CONSERVERAIS +CONSERVERONT +CONSIDERA +CONSIDERAS +CONSIDERERA +CONSIGNAIS +CONSIGNAT +CONSIGNE +CONSIGNERAS +CONSIGNIONS +CONSISTANT +CONSISTAT +CONSISTERAS +CONSISTIONS +CONSOEURS +CONSOLANT +CONSOLAT +CONSOLEES +CONSOLEREZ +CONSOLIDAI +CONSOLIDER +CONSOLONS +CONSOMMAIT +CONSOMMATES +CONSOMMEE +CONSOMMERENT +CONSOMMONS +CONSONAI +CONSONERAIT +CONSONIEZ +CONSORTIALES +CONSPIRAIENT +CONSPIRATION +CONSPIRER +CONSPUAIENT +CONSPUES +CONSTANT +CONSTATAMES +CONSTATATION +CONSTATERAIS +CONSTATEZ +CONSTELLAMES +CONSTELLEZ +CONSTERNANT +CONSTERNAT +CONSTERNERAI +CONSTIPAIT +CONSTIPER +CONSTITUER +CONSTITUTION +CONSTRUIREZ +CONSTRUISE +CONSULTA +CONSULTANTES +CONSULTATIF +CONSULTER +CONSULTRICE +CONSUMANT +CONSUMEE +CONSUMERENT +CONSUMES +CONTACTAI +CONTACTERAI +CONTACTERONT +CONTAGIONNA +CONTAGIONNAT +CONTAMINAIT +CONTAMINE +CONTAMINERAS +CONTAMINIONS +CONTATES +CONTEMPLANT +CONTEMPLEE +CONTEMPLONS +CONTENAIENT +CONTENEURISA +CONTENEURISE +CONTENT +CONTENTEREZ +CONTENTIEZ +CONTENUE +CONTERENT +CONTESTAI +CONTESTASSE +CONTESTERA +CONTESTERONS +CONTEUSES +CONTEXTUEL +CONTIENDRAIS +CONTIENNES +CONTINENCE +CONTINENTES +CONTINRENT +CONTINUA +CONTINUASSES +CONTINUER +CONTINUITES +CONTONDANTE +CONTORSIONS +CONTOURNASSE +CONTOURNONS +CONTRACTAI +CONTRACTASSE +CONTRACTENT +CONTRACTUREE +CONTRAIGNANT +CONTRAINDRA +CONTRAIS +CONTRARIAIS +CONTRARIERA +CONTRASTA +CONTRASTAS +CONTRASTEES +CONTRASTEREZ +CONTRASTIONS +CONTRE +CONTREBATTIT +CONTREBOUTE +CONTREBRAQUA +CONTREBRAQUE +CONTREBUTANT +CONTREBUTEES +CONTREBUTONS +CONTRECLE +CONTRECOLLE +CONTRECOUP +CONTREDIRAIS +CONTREDIS +CONTREDISES +CONTREDIT +CONTREFERA +CONTREFERONT +CONTREFIRENT +CONTREFORT +CONTREFOUTES +CONTREFOUTRE +CONTREMAITRE +CONTREMANDAS +CONTREMANDES +CONTREPET +CONTREPLAQUA +CONTREPLAQUE +CONTREPROJET +CONTRERENT +CONTRESEINGS +CONTRESIGNEZ +CONTRETYPAIS +CONTREVENANT +CONTREVENTA +CONTREVENTAT +CONTREVIENT +CONTREVINTES +CONTRIBUANT +CONTRIBUENT +CONTRIONS +CONTRISTER +CONTRITES +CONTROLAIENT +CONTROLENT +CONTROLERIEZ +CONTROLIEZ +CONTROUVAIT +CONTROUVATES +CONTROUVIEZ +CONTUMACES +CONTUSIONNE +CONTUSIONS +CONVAINCRA +CONVAINCRONS +CONVAINQUANT +CONVECTIVES +CONVENEZ +CONVENTION +CONVENUES +CONVERGEAS +CONVERGENT +CONVERGERAIT +CONVERGIEZ +CONVERSAS +CONVERSERENT +CONVERSIEZ +CONVERTIES +CONVERTIREZ +CONVERTISSEZ +CONVEXITE +CONVIASSE +CONVICTS +CONVIENDREZ +CONVIER +CONVIERIONS +CONVINSSE +CONVIVIAL +CONVOIENT +CONVOIERONS +CONVOITAIT +CONVOITATES +CONVOITERAIT +CONVOITEURS +CONVOLAI +CONVOLASSIEZ +CONVOLERAIS +CONVOLEZ +CONVOQUAS +CONVOQUEES +CONVOQUEREZ +CONVOYA +CONVOYASSE +CONVOYER +CONVOYONS +CONVULSER +CONVULSIONNE +COOCCUPANTS +COOLIE +COOPERANTES +COOPERATEUR +COOPERATRICE +COOPERERAS +COOPERIONS +COOPTASSE +COOPTEE +COOPTERENT +COOPTONS +COORDINENCES +COORDONNANTS +COORDONNES +COORGANISEE +COPAHU +COPARENT +COPARTAGEAI +COPARTAGER +COPERMUTAIT +COPERMUTATES +COPERMUTIEZ +COPIAGE +COPIASSENT +COPIER +COPIERIONS +COPIEZ +COPILOTAMES +COPILOTE +COPILOTERAS +COPILOTIONS +COPINANT +COPINENT +COPINERIE +COPINIEZ +COPLAS +COPOSSEDAS +COPOSSEDEES +COPOSSEDEREZ +COPOSSESSEUR +COPRESIDA +COPRESIDENT +COPRESIDERAS +COPRESIDIONS +COPROCESSEUR +COPRODUIRA +COPRODUIRONS +COPRODUISEZ +COPRODUISIT +COPROLITHES +COPROPHAGIES +COPROSTEROLS +COPULAIT +COPULATES +COPULATRICES +COPULERENT +COPULONS +COQUATRES +COQUELEUSE +COQUELUCHES +COQUERET +COQUETA +COQUETASSES +COQUETEZ +COQUETTEMENT +COQUETTERIES +COQUILLAIENT +COQUILLASSE +COQUILLENT +COQUILLERIEZ +COQUILLIERES +COQUINET +CORACOIDE +CORALLIENNE +CORALLINE +CORANISAIT +CORANISATES +CORANISERAIT +CORANISIEZ +CORBILLARD +CORDAGE +CORDASSENT +CORDEE +CORDELASSE +CORDELER +CORDELIEZ +CORDELLEREZ +CORDERAI +CORDERIONS +CORDIALE +CORDIERITES +CORDOBA +CORDONNAI +CORDONNERAI +CORDONNIERS +CORDOUANS +COREENNES +COREGONE +CORFIOTES +CORICIDES +CORMES +CORNACEE +CORNALINES +CORNAQUAS +CORNAQUEES +CORNAQUEREZ +CORNARD +CORNAT +CORNEILLARDS +CORNEMENTS +CORNERA +CORNERONS +CORNEUR +CORNICHON +CORNILLONS +COROLLAIRE +CORONARIEN +CORONAVIRUS +COROSSOL +CORPORATION +CORPOREITES +CORPULENCE +CORPUSCULES +CORRECTEURS +CORRECTRICE +CORRELAMES +CORRELATEUR +CORRELATIVES +CORRELERAIS +CORRELEZ +CORRESPONDU +CORRIGEA +CORRIGEASSE +CORRIGEONS +CORRIGERIEZ +CORRIGIBLE +CORROBORANT +CORROBORAT +CORROBOREES +CORROBOREREZ +CORRODA +CORRODAS +CORRODEES +CORRODEREZ +CORROI +CORROIERIE +CORROMPAIT +CORROMPIS +CORROMPRAI +CORROMPRONT +CORROSIONS +CORROYAMES +CORROYE +CORROYEUSE +CORRUPTIBLE +CORS +CORSAMES +CORSE +CORSERAIS +CORSET +CORSETASSENT +CORSETER +CORSETERIES +CORSETIERS +CORSOPHONE +CORTICAUX +CORTINAIRE +CORTON +CORVEE +CORYDALIS +COSAQUES +COSIGNANT +COSIGNATES +COSIGNES +COSMETIQUAIT +COSMETIQUE +COSMETIQUONS +COSMOCHIMIES +COSMOLOGUES +COSSANT +COSSASSIONS +COSSERAIT +COSSETTES +COSSUES +COSTAUD +COSTUMAI +COSTUMASSIEZ +COSTUMERAI +COSTUMERONT +COSY +COTANGENTE +COTATES +COTELEES +COTERAIT +COTES +COTICES +COTIERES +COTINGAS +COTIRAS +COTISAIENT +COTISASSENT +COTISEES +COTISEREZ +COTISSAIENT +COTISSONS +COTOIERAI +COTOIES +COTONNAIT +COTONNATES +COTONNERAIT +COTONNES +COTONNIONS +COTOYAIT +COTOYATES +COTOYONS +COTTAI +COTTASSIEZ +COTTERAI +COTTERONT +COTUTEUR +COTYLEDONS +COUACS +COUCHAGE +COUCHAILLANT +COUCHAILLER +COUCHANT +COUCHAT +COUCHERAIS +COUCHERONT +COUCHIEZ +COUCOU +COUDAI +COUDASSIEZ +COUDERAI +COUDERONT +COUDOIENT +COUDOIERONS +COUDOYAIT +COUDOYATES +COUDOYONS +COUDRES +COUDS +COUFFA +COUGNOU +COUILLONNA +COUILLONNER +COUILLUES +COUINASSE +COUINENT +COUINERIEZ +COULABILITE +COULANTE +COULATES +COULERAIENT +COULES +COULISSAIS +COULISSASSES +COULISSEMENT +COULISSERENT +COULISSIEZ +COULOMBS +COUMARINES +COUPAGE +COUPAILLANT +COUPAILLEE +COUPAILLONS +COUPASSAI +COUPASSERAI +COUPASSERONT +COUPEES +COUPELLEES +COUPELLEREZ +COUPEMENT +COUPERENT +COUPEROSES +COUPEUSE +COUPLAIS +COUPLAT +COUPLERAI +COUPLERONT +COUPOIR +COUPS +COURAGEUSES +COURAILLAMES +COURAILLE +COURAILLEREZ +COURAILLEUX +COURANTES +COURATANT +COURATENT +COURATERIEZ +COURBAGE +COURBARINE +COURBATTU +COURBATTURAS +COURBATTURES +COURBATURAI +COURBATUREZ +COURBENT +COURBERIEZ +COURBEZ +COURCAILLAT +COURCAILLIEZ +COURETTE +COURGETTES +COURONNA +COURONNASSES +COURONNENT +COURONNERIEZ +COUROS +COURRERIES +COURROIE +COURROUCANT +COURROUCEE +COURROUCONS +COURSAS +COURSEES +COURSEREZ +COURSIERES +COURSONS +COURTAUDAMES +COURTAUDE +COURTAUDERAS +COURTAUDIONS +COURTIERE +COURTINES +COURTISAT +COURTISERAIS +COURTISEZ +COURTRAISIEN +COURUS +COUSAIS +COUSES +COUSIN +COUSINAMES +COUSINE +COUSINERAS +COUSINIONS +COUSISSIONS +COUSU +COUTAMES +COUTATES +COUTELIERS +COUTERAS +COUTEUSES +COUTS +COUTURAIENT +COUTURES +COUVADE +COUVAMES +COUVE +COUVERAIENT +COUVERONS +COUVEUSES +COUVRAIS +COUVRES +COUVRIRA +COUVRIRONS +COUVRONS +COVALENTS +COVENANTS +COVOITURAIS +COVOITURAT +COVOITUREZ +COXAI +COXALGIQUES +COXASSES +COXER +COXERIONS +COYAU +CRABIERES +CRABOTAMES +CRABOTE +CRABOTERAS +CRABOTIONS +CRACHANT +CRACHE +CRACHERAIS +CRACHEUR +CRACHINE +CRACHOTAI +CRACHOTASSE +CRACHOTEMENT +CRACHOTERENT +CRACHOTONS +CRACOVIENNE +CRADOQUE +CRAIGNANT +CRAIGNISSE +CRAILLAI +CRAILLASSIEZ +CRAILLERAI +CRAILLERONT +CRAINDRAIT +CRAINTE +CRAMAIENT +CRAMASSIONS +CRAMEES +CRAMEREZ +CRAMIONS +CRAMPAIENT +CRAMPASSIONS +CRAMPERAIENT +CRAMPES +CRAMPONNAIS +CRAMPONNENT +CRAMPONNONS +CRANAS +CRANEES +CRANERENT +CRANEURS +CRANIEZ +CRANIOSCOPIE +CRANTA +CRANTASSE +CRANTENT +CRANTERIEZ +CRANTIEZ +CRAPAHUTAS +CRAPAHUTEES +CRAPAHUTEREZ +CRAPAUD +CRAPAUTAIENT +CRAPAUTES +CRAPOTAI +CRAPOTASSIEZ +CRAPOTERAIS +CRAPOTEUSE +CRAPOUSSINE +CRAPULEUSES +CRAQUANT +CRAQUAT +CRAQUELAIS +CRAQUELAT +CRAQUELEZ +CRAQUELLERAI +CRAQUELLES +CRAQUERAIENT +CRAQUES +CRAQUETERENT +CRAQUETTERAI +CRAQUETTES +CRASHA +CRASHASSES +CRASHERA +CRASHERONS +CRASSA +CRASSASSE +CRASSENT +CRASSERIEZ +CRASSIERS +CRATERELLE +CRATONS +CRAVACHANT +CRAVACHEE +CRAVACHERENT +CRAVACHONS +CRAVATASSENT +CRAVATER +CRAVATERIONS +CRAWL +CRAWLASSENT +CRAWLER +CRAWLERIONS +CRAWLIONS +CRAYONNAGE +CRAYONNER +CRAYONNIONS +CREANCE +CREASSES +CREATINES +CREATIQUES +CRECELLES +CRECHAS +CRECHER +CRECHERIONS +CREDIBILISA +CREDIBILISAT +CREDIT +CREDITASSENT +CREDITER +CREDITERIONS +CREDITISTES +CREDULITE +CREERAI +CREERONT +CREMAILLERE +CREMASSES +CREMATISTES +CREMERA +CREMERIEZ +CREMIERE +CRENAGES +CRENASSES +CRENELA +CRENELASSE +CRENELER +CRENELONS +CRENERAS +CRENIONS +CREOLISAI +CREOLISASSE +CREOLISEE +CREOLISERENT +CREOLISME +CREOSOL +CREOSOTANT +CREOSOTEE +CREOSOTERENT +CREOSOTONS +CREPAS +CREPEES +CREPELURE +CREPERAS +CREPEZ +CREPIEZ +CREPIRAI +CREPIRONT +CREPISSES +CREPITAIENT +CREPITASSENT +CREPITEMENTS +CREPITEREZ +CREPON +CREPUSCULE +CRESYLS +CRETAIT +CRETATES +CRETERAIENT +CRETES +CRETINISEES +CRETINISEREZ +CRETINISMES +CRETONNE +CREUSAIT +CREUSATES +CREUSERAIENT +CREUSES +CREUSURE +CREVAISONS +CREVARDS +CREVASSASSE +CREVASSENT +CREVASSERIEZ +CREVATES +CREVERAIT +CREVETTES +CREVOTAIS +CREVOTAT +CREVOTERAS +CREVOTIONS +CRIAILLAIENT +CRIAILLERONS +CRIAILLONS +CRIARDES +CRIB +CRIBLAS +CRIBLEES +CRIBLEREZ +CRIBLEZ +CRICOIDE +CRIERA +CRIERONS +CRIME +CRIMINALISES +CRIMINEL +CRINIERES +CRIQUA +CRIQUASSES +CRIQUERAIENT +CRIQUES +CRISAIENT +CRISASSIONS +CRISERAIT +CRISEUSES +CRISPAIT +CRISPASSIEZ +CRISPER +CRISPERIONS +CRISS +CRISSASSENT +CRISSER +CRISSERIONS +CRITHMUM +CRITICAILLEZ +CRITIQUA +CRITIQUASSE +CRITIQUENT +CRITIQUERIEZ +CRITIQUIEZ +CROASSAS +CROASSEMENTS +CROASSEREZ +CROATE +CROCHAIS +CROCHAT +CROCHERAIS +CROCHET +CROCHETAMES +CROCHETE +CROCHETERAS +CROCHETEUSE +CROCHEUSES +CROCHIRAI +CROCHIRONT +CROCHISSIEZ +CROCHUS +CROCUS +CROIRE +CROISAI +CROISASSIEZ +CROISER +CROISERIONS +CROISIERES +CROISSAIT +CROISSENT +CROITRAI +CROITRONT +CROLLAS +CROLLEES +CROLLEREZ +CROMALIN +CROQUA +CROQUAS +CROQUEES +CROQUENOTS +CROQUEREZ +CROQUEUR +CROSS +CROSSASSENT +CROSSER +CROSSERIONS +CROSSMANS +CROTCHONS +CROTTAS +CROTTEES +CROTTEREZ +CROTTIONS +CROULANT +CROULAT +CROULERAI +CROULERONT +CROUPALE +CROUPIERES +CROUPIONNENT +CROUPIRA +CROUPIRONS +CROUPISSE +CROUPITES +CROUTAIENT +CROUTASSIONS +CROUTERAIENT +CROUTES +CROWN +CROYANT +CRU +CRUCHONS +CRUCIFIAI +CRUCIFIER +CRUCIFIXIONS +CRUEL +CRUISER +CRURALES +CRUSSIONS +CRUTES +CRYOGENES +CRYOMETRIQUE +CRYOTRON +CRYPTAIT +CRYPTASSIONS +CRYPTERAIENT +CRYPTES +CRYPTOMERIAS +CTENAIRES +CUBAI +CUBAS +CUBE +CUBERAIS +CUBERONT +CUBIS +CUBITAUX +CUCHAULES +CUCURBITACEE +CUCUTERIES +CUEILLENT +CUEILLERONS +CUEILLI +CUEILLISSENT +CUESTAS +CUILLERE +CUIRAIENT +CUIRASSANT +CUIRASSEE +CUIRASSERAIT +CUIRASSIER +CUIRIONS +CUISANTES +CUISINA +CUISINASSES +CUISINERA +CUISINERONS +CUISINIEZ +CUISISSES +CUISSARDS +CUISTANCE +CUITA +CUITASSES +CUITERA +CUITERONS +CUIVRAGES +CUIVRASSES +CUIVRERA +CUIVRERIEZ +CUIVRIONS +CULAMES +CULAT +CULBUTANT +CULBUTEE +CULBUTERAIT +CULBUTEURS +CULENT +CULERIEZ +CULIERE +CULMINAIS +CULMINASSES +CULMINERA +CULMINERONS +CULOTTA +CULOTTASSE +CULOTTENT +CULOTTERIEZ +CULOTTIEZ +CULPABILISE +CULPABILITE +CULTISMES +CULTIVAR +CULTIVATEUR +CULTIVERAI +CULTIVERONT +CULTURAL +CULTURELLE +CULTURISTE +CUMULABLE +CUMULARDES +CUMULATIF +CUMULERA +CUMULERONS +CUNICOLES +CUPIDES +CUPRIFERES +CURAGE +CURARE +CURARISANT +CURARISAT +CURARISERAI +CURARISERONT +CURASSIEZ +CURATIVE +CUREES +CUREREZ +CURETAIENT +CURETASSIONS +CURETIEZ +CUREUR +CURIAUX +CURIEUX +CURLEUR +CURRICULUMS +CURSUS +CUSCUTACEES +CUSSONNES +CUSTOMISANT +CUSTOMISIEZ +CUTICULAIRE +CUTTER +CUVAIT +CUVATES +CUVELAIENT +CUVELASSIONS +CUVELIEZ +CUVELLEREZ +CUVERAI +CUVERIONS +CUVIONS +CYANEES +CYANOSAIENT +CYANOSES +CYANURAIT +CYANURATES +CYANURES +CYBERCAMERAS +CYBERGUERRES +CYBERSPATIAL +CYCLABLES +CYCLANIQUES +CYCLINES +CYCLISANT +CYCLISATIONS +CYCLISERAIT +CYCLISIEZ +CYCLOALCENE +CYCLOPEEN +CYCLOPIENNES +CYCLORAMAS +CYLINDRAGES +CYLINDRASSES +CYLINDRENT +CYLINDRERIEZ +CYLINDRIEZ +CYMAISES +CYMBALISTES +CYMRIQUES +CYNIQUES +CYNOLOGIES +CYONS +CYPRIERE +CYRILLIQUES +CYSTICERQUES +CYSTOSTOMIES +CYTISES +CYTOLOGIQUES +CYTOPENIE +CYTOTROPISME +DABAS +DACITES +DACRYADENITE +DACTYLO +DACTYLOS +DAGUA +DAGUASSES +DAGUERA +DAGUERONS +DAGUET +DAHIRS +DAIGNA +DAIGNASSES +DAIGNERAIENT +DAIGNES +DAILLE +DAINES +DAKINS +DALLAIENT +DALLASSIONS +DALLERAIENT +DALLES +DALMATIENNE +DALTONIENNES +DAMAIENT +DAMASCENE +DAMASQUINANT +DAMASQUINEES +DAMASQUINIEZ +DAMASSAMES +DAMASSE +DAMASSERAS +DAMASSEZ +DAME +DAMERAS +DAMEUSE +DAMNAI +DAMNASSIEZ +DAMNER +DAMNERIONS +DAMOISELLE +DANCES +DANDINAMES +DANDINERA +DANDINERONS +DANDY +DANGEROSITES +DANOISES +DANSANT +DANSAT +DANSERAIS +DANSEUR +DANSOTAIENT +DANSOTERAIT +DANSOTIEZ +DANSOTTAS +DANSOTTER +DANTONISTE +DAPHNIE +DARBYSTE +DARDAMES +DARDE +DARDERAS +DARDILLON +DARIS +DARONS +DARTRE +DARWINIENS +DATABLE +DATAMES +DATATION +DATERAI +DATERIONS +DATIF +DATTIERS +DAUBAS +DAUBEES +DAUBEREZ +DAUBEZ +DAUPHINELLES +DAVANTAGE +DAYAKS +DEALAIS +DEALAT +DEALERAIS +DEALES +DEAMBULA +DEAMBULASSES +DEAMBULERENT +DEAMBULONS +DEBACHANT +DEBACHEE +DEBACHERENT +DEBACHONS +DEBACLASSENT +DEBACLEMENTS +DEBACLEREZ +DEBADGE +DEBADGER +DEBADGERIONS +DEBAGOULAI +DEBAGOULERAI +DEBAGUAIT +DEBAGUATES +DEBAGUERAIT +DEBAGUIEZ +DEBAILLONNAS +DEBAILLONNES +DEBALLAIS +DEBALLASTAGE +DEBALLERAI +DEBALLERONT +DEBALLONNAI +DEBALLONNEZ +DEBALOURDEE +DEBANALISA +DEBANALISER +DEBANDAI +DEBANDASSIEZ +DEBANDERAI +DEBANDERONT +DEBAPTISAIT +DEBAPTISATES +DEBAPTISIEZ +DEBARCADERE +DEBARDANT +DEBARDEE +DEBARDERENT +DEBARDEUSES +DEBAROULAMES +DEBAROULE +DEBAROULERAS +DEBAROULIONS +DEBARQUASSE +DEBARQUEMENT +DEBARQUERENT +DEBARQUONS +DEBARRASSAI +DEBARRASSEZ +DEBARRER +DEBARRERIONS +DEBARULAIENT +DEBARULES +DEBATAIT +DEBATATES +DEBATERAIT +DEBATEZ +DEBATIRAIENT +DEBATIS +DEBATISSIONS +DEBATTE +DEBATTIEZ +DEBATTIT +DEBATTREZ +DEBAUCHAGE +DEBAUCHER +DEBECQUETAT +DEBECQUETIEZ +DEBECTAS +DEBECTEES +DEBECTEREZ +DEBENTURE +DEBENZOLANT +DEBENZOLEE +DEBENZOLONS +DEBEQUETER +DEBILE +DEBILITAMES +DEBILITERA +DEBILITERONS +DEBILLARDAIS +DEBINANT +DEBINEE +DEBINERENT +DEBINEUSES +DEBITA +DEBITANT +DEBITAT +DEBITERAIS +DEBITEUR +DEBITRICE +DEBLAIS +DEBLATERER +DEBLAYAGES +DEBLAYASSES +DEBLAYERA +DEBLAYERONS +DEBLOQUAI +DEBLOQUASSE +DEBLOQUENT +DEBLOQUERIEZ +DEBOBINAI +DEBOBINERAI +DEBOBINERONT +DEBOGUAIENT +DEBOGUES +DEBOISAGE +DEBOISASSENT +DEBOISEMENTS +DEBOISEREZ +DEBOITA +DEBOITASSE +DEBOITEMENT +DEBOITERENT +DEBOITONS +DEBONDASSENT +DEBONDER +DEBONDERIONS +DEBONNAIRES +DEBORDANT +DEBORDAT +DEBORDERAI +DEBORDERONT +DEBOSSAI +DEBOSSASSIEZ +DEBOSSELAI +DEBOSSELEUR +DEBOSSELONS +DEBOSSEREZ +DEBOSSEZ +DEBOTTANT +DEBOTTEE +DEBOTTERENT +DEBOTTONS +DEBOUCHAS +DEBOUCHEES +DEBOUCHERAS +DEBOUCHEZ +DEBOUCLAIT +DEBOUCLATES +DEBOUCLERAIT +DEBOUCLIEZ +DEBOUILLEZ +DEBOUILLIS +DEBOUILLONS +DEBOULASSENT +DEBOULER +DEBOULERIONS +DEBOULONNAI +DEBOULONNERA +DEBOUQUAIS +DEBOUQUAT +DEBOUQUERAIS +DEBOUQUEZ +DEBOURBAIT +DEBOURBATES +DEBOURBERAIT +DEBOURBEURS +DEBOURRAGE +DEBOURREREZ +DEBOURREZ +DEBOURSAMES +DEBOURSE +DEBOURSERAIS +DEBOURSEZ +DEBOUSSOLEE +DEBOUT +DEBOUTASSENT +DEBOUTEMENTS +DEBOUTEREZ +DEBOUTONNAGE +DEBOUTONNERA +DEBRAGUETTEE +DEBRAIENT +DEBRAIERONS +DEBRAILLAS +DEBRAILLEES +DEBRAILLEREZ +DEBRANCHA +DEBRANCHENT +DEBRASAGE +DEBRASASSENT +DEBRASER +DEBRASERIONS +DEBRAYABLES +DEBRAYASSE +DEBRAYENT +DEBRAYERIEZ +DEBREAKAI +DEBREAKERAI +DEBREAKERONT +DEBRIDAIENT +DEBRIDERA +DEBRIDERONS +DEBRIEFAIS +DEBRIEFAT +DEBRIEFERAIS +DEBRIEFEZ +DEBROCHAI +DEBROCHERAI +DEBROCHERONT +DEBRONZAIT +DEBRONZATES +DEBRONZERAIT +DEBRONZIEZ +DEBROUILLER +DEBROUSSAI +DEBROUSSAIS +DEBROUSSAT +DEBROUSSERAI +DEBRUTIR +DEBRUTIRIONS +DEBRUTISSES +DEBUCHAIENT +DEBUCHERS +DEBUDGETISEE +DEBUGGAI +DEBUGGASSIEZ +DEBUGGERAI +DEBUGGERONT +DEBUSQUAGE +DEBUSQUEREZ +DEBUT +DEBUTANISAIS +DEBUTASSIEZ +DEBUTERAI +DEBUTERONT +DECABRISTE +DECACHETAIT +DECACHETATES +DECACHETONS +DECADENASSEE +DECADENCES +DECADRA +DECADRASSE +DECADRENT +DECADRERIEZ +DECAEDRES +DECAFEINER +DECAGONALES +DECAISSAIT +DECAISSATES +DECAISSES +DECALAIS +DECALAMINEE +DECALANT +DECALCIFIAI +DECALCIFIENT +DECALE +DECALERAS +DECALIONS +DECALOTTAIT +DECALOTTATES +DECALOTTIEZ +DECALQUAMES +DECALQUE +DECALQUERAS +DECALQUIONS +DECAMPA +DECAMPASSES +DECAMPERA +DECAMPERONS +DECANALES +DECANILLANT +DECANILLENT +DECANOIQUES +DECANTANT +DECANTATIONS +DECANTERAIT +DECANTEURS +DECAPAI +DECAPASSE +DECAPELA +DECAPELASSES +DECAPELES +DECAPEMENTS +DECAPEREZ +DECAPEZ +DECAPITAT +DECAPITERAI +DECAPITERONT +DECAPONS +DECAPOTAS +DECAPOTEES +DECAPOTEREZ +DECAPSULA +DECAPSULASSE +DECAPSULEE +DECAPSULIEZ +DECARBONATE +DECARBURANTE +DECARBURATES +DECARBURES +DECARCASSEE +DECARRELAIT +DECARRELATES +DECARRELLE +DECASYLLABES +DECATIE +DECATIRENT +DECATISSAIS +DECATISSEZ +DECAUSAIT +DECAUSATES +DECAUSERAIT +DECAUSIEZ +DECAVASSE +DECAVENT +DECAVERIEZ +DECCAS +DECEDASSENT +DECEDER +DECEDERIONS +DECELABLES +DECELASSES +DECELENT +DECELERASSE +DECELERENT +DECELERERIEZ +DECELES +DECEMVIR +DECENNAIRE +DECENTES +DECENTRAT +DECENTRER +DECEPTIFS +DECERCLAMES +DECERCLE +DECERCLERAS +DECERCLIONS +DECEREBRASSE +DECEREBREE +DECEREBRONS +DECERNASSENT +DECERNER +DECERNERIONS +DECERVELAGES +DECERVELES +DECEVAIS +DECEVONS +DECEVRONS +DECHAINASSE +DECHAINEMENT +DECHAINERENT +DECHAINONS +DECHANTASSE +DECHANTERA +DECHANTERONS +DECHARGEAS +DECHARGEMENT +DECHARGERAIS +DECHARGEUR +DECHARNAMES +DECHARNE +DECHARNERAIS +DECHARNEZ +DECHAUMAIT +DECHAUMATES +DECHAUMERAIT +DECHAUMEUSES +DECHAUSSAIS +DECHAUSSAT +DECHAUSSERAI +DECHAUX +DECHEVETRAI +DECHEVETREZ +DECHIFFONNER +DECHIFFRAGES +DECHIFFRENT +DECHIFFRIEZ +DECHIQUETEE +DECHIQUETIEZ +DECHIRAI +DECHIRASSE +DECHIREMENT +DECHIRERENT +DECHIRONS +DECHLORURAS +DECHLORURENT +DECHOIE +DECHOIREZ +DECHOQUAI +DECHOQUERAI +DECHOQUERONT +DECHUSSIEZ +DECIDABLE +DECIDASSENT +DECIDENT +DECIDERIEZ +DECIDIEZ +DECIDUS +DECILITRES +DECILLASSENT +DECILLER +DECILLERIONS +DECIMAIENT +DECIMALISAIT +DECIMALISIEZ +DECIMASSES +DECIME +DECIMERAS +DECIMETRIQUE +DECINTRAI +DECINTRER +DECISIFS +DECISIVEMENT +DECLAMAMES +DECLAMATEUR +DECLAMEES +DECLAMEREZ +DECLARA +DECLARANTES +DECLARATIF +DECLAREES +DECLAREREZ +DECLASSA +DECLASSASSES +DECLASSENT +DECLASSERIEZ +DECLASSIFIEE +DECLASSONS +DECLENCHAI +DECLENCHASSE +DECLINAIS +DECLINASSE +DECLINES +DECLINQUAIS +DECLINQUAT +DECLINQUEZ +DECLIQUETAIS +DECLIQUETONS +DECLOISONNEE +DECLORA +DECLORONS +DECLOSONS +DECLOUANT +DECLOUEE +DECLOUERENT +DECLOUONS +DECOCHANT +DECOCHEE +DECOCHERAIT +DECOCHIEZ +DECODAI +DECODASSIEZ +DECODERAI +DECODERONT +DECOFFRA +DECOFFRASSE +DECOFFRENT +DECOFFRERIEZ +DECOHABITAI +DECOHABITEZ +DECOIFFAIENT +DECOIFFEREZ +DECOINCA +DECOINCASSE +DECOINCEMENT +DECOINCERENT +DECOINCONS +DECOLERAIT +DECOLERATES +DECOLERERENT +DECOLERONS +DECOLLAMES +DECOLLATION +DECOLLES +DECOLLETAS +DECOLLETEES +DECOLLETIONS +DECOLLONS +DECOLONISENT +DECOLORAIENT +DECOLOREES +DECOLOREREZ +DECOMBRES +DECOMMANDERA +DECOMMETTAIT +DECOMMETTRAI +DECOMMUNISAS +DECOMMUNISER +DECOMPACTAT +DECOMPACTIEZ +DECOMPENSAS +DECOMPENSEE +DECOMPLEXA +DECOMPLEXES +DECOMPOSAIS +DECOMPOSAT +DECOMPOSEUR +DECOMPRESSE +DECOMPRIMAS +DECOMPRIMENT +DECOMPTAIENT +DECOMPTES +DECONCERTERA +DECONFIRE +DECONFISAIT +DECONFISSENT +DECONGELA +DECONGELENT +DECONGESTIFS +DECONNAIENT +DECONNASSENT +DECONNECTAIS +DECONNERENT +DECONNEUSES +DECONSEILLEE +DECONSIDERAI +DECONSIGNER +DECONSTIPAIS +DECORA +DECORASSES +DECORATIONS +DECORDAMES +DECORDE +DECORDERAS +DECORDIONS +DECORERAIS +DECOREZ +DECORNAS +DECORNEES +DECORNEREZ +DECORONS +DECORRELEES +DECORRELEREZ +DECORS +DECORTIQUEE +DECOTAI +DECOTASSIEZ +DECOTERAI +DECOTERONT +DECOUCHAIT +DECOUCHATES +DECOUCHERENT +DECOUCHONS +DECOUDRIEZ +DECOUENNAMES +DECOUENNE +DECOUENNERAS +DECOUENNIONS +DECOULASSE +DECOULERA +DECOULERONS +DECOUPAI +DECOUPASSIEZ +DECOUPERAI +DECOUPERONT +DECOUPLAGE +DECOUPLER +DECOUPONS +DECOURAGEANT +DECOURAGES +DECOURONNANT +DECOURONNEES +DECOURONNONS +DECOUSIEZ +DECOUSIT +DECOUVERTE +DECOUVRENT +DECOUVRIR +DECOUVRITES +DECRAMPONNEZ +DECRASSAIT +DECRASSATES +DECRASSES +DECREDITANT +DECREDITEE +DECREDITONS +DECREMENTER +DECREPAGES +DECREPASSES +DECREPERA +DECREPERONS +DECREPIR +DECREPIRIONS +DECREPISSE +DECREPITERA +DECREPONS +DECRETALES +DECRETATES +DECRETERAIT +DECRETIEZ +DECREUSAIT +DECREUSATES +DECREUSES +DECRIAIT +DECRIATES +DECRIERAIT +DECRIIEZ +DECRIRA +DECRIRONS +DECRISPAS +DECRISPE +DECRISPERAS +DECRISPIONS +DECRIVE +DECRIVISSENT +DECROCHAI +DECROCHER +DECROCHIONS +DECROISAS +DECROISEES +DECROISERAS +DECROISIONS +DECROISSANTS +DECROIT +DECROITRIONS +DECROTTAIT +DECROTTATES +DECROTTERAIT +DECROTTEURS +DECROTTONS +DECRUANT +DECRUEE +DECRUERENT +DECRUMES +DECRUSAIT +DECRUSATES +DECRUSERAIT +DECRUSIEZ +DECRYPTA +DECRYPTASSE +DECRYPTEMENT +DECRYPTERENT +DECRYPTONS +DECUIRAS +DECUISAIT +DECUISIS +DECUITE +DECUIVRAMES +DECUIVRE +DECUIVRERAS +DECUIVRIONS +DECULASSASSE +DECULASSENT +DECULOTTAGE +DECULOTTER +DECUPLA +DECUPLASSES +DECUPLENT +DECUPLERIEZ +DECURIE +DECUVA +DECUVANT +DECUVEE +DECUVERENT +DECUVONS +DEDAIGNAMES +DEDAIGNE +DEDAIGNERAS +DEDAIGNEUSES +DEDALEENNE +DEDIAMES +DEDICACA +DEDICACASSES +DEDICACERA +DEDICACERONS +DEDICATOIRES +DEDIERAIT +DEDIONS +DEDIRIEZ +DEDISENT +DEDISSIONS +DEDOMMAGEAIT +DEDOMMAGEE +DEDOMMAGEZ +DEDORAMES +DEDORE +DEDORERAS +DEDORIONS +DEDOUANAIT +DEDOUANATES +DEDOUANES +DEDOUBLAIS +DEDOUBLAT +DEDOUBLERAI +DEDOUBLERONT +DEDRAMATISEE +DEDUIRAI +DEDUIRONT +DEDUISIEZ +DEDUISIT +DEFAILLAIS +DEFAILLES +DEFAILLIRAIS +DEFAILLISSE +DEFAISAIENT +DEFAITE +DEFALQUAI +DEFALQUERAI +DEFALQUERONT +DEFASSES +DEFATIGUAIS +DEFATIGUAT +DEFATIGUEZ +DEFAUFILANT +DEFAUFILEE +DEFAUFILONS +DEFAUSSER +DEFAVEUR +DEFAVORISEE +DEFECATION +DEFECTUEUSE +DEFENDAIS +DEFENDEZ +DEFENDRE +DEFENDUS +DEFENESTRENT +DEFENSES +DEFEQUA +DEFEQUASSES +DEFEQUERA +DEFEQUERONS +DEFERAIS +DEFERAT +DEFERENTE +DEFERERENT +DEFERLA +DEFERLANTES +DEFERLE +DEFERLERAIS +DEFERLEZ +DEFERRAIENT +DEFERRERA +DEFERRERONS +DEFEUILLAMES +DEFEUILLE +DEFEUILLERAS +DEFEUILLIONS +DEFEUTRANT +DEFEUTREE +DEFEUTRERENT +DEFEUTRONS +DEFIANT +DEFIAT +DEFIBRANT +DEFIBREE +DEFIBRERENT +DEFIBREUSES +DEFICELAI +DEFICELEZ +DEFICELLERAS +DEFICIENT +DEFIEES +DEFIEREZ +DEFIGEAIENT +DEFIGERAIENT +DEFIGES +DEFIGURANT +DEFIGURES +DEFILAI +DEFILASSIEZ +DEFILER +DEFILERIONS +DEFILIONS +DEFINIRAIENT +DEFINIS +DEFINISSES +DEFINITIFS +DEFINITOIRES +DEFISCALISEE +DEFISSENT +DEFLAGRAIT +DEFLAGRER +DEFLATAIENT +DEFLATES +DEFLECHIES +DEFLECHIREZ +DEFLECHISSE +DEFLECTEURS +DEFLEURIRA +DEFLEURIRONS +DEFLEURISSEZ +DEFLOQUA +DEFLOQUASSES +DEFLOQUERA +DEFLOQUERONS +DEFLORAIS +DEFLORASSIEZ +DEFLORER +DEFLORERIONS +DEFLUVIATION +DEFOLIANTES +DEFOLIATION +DEFOLIERAIS +DEFOLIEZ +DEFONCAIT +DEFONCATES +DEFONCES +DEFORCAIENT +DEFORCES +DEFORESTAIS +DEFORESTAT +DEFORESTERAI +DEFORMAIENT +DEFORMASSENT +DEFORMATRICE +DEFORMERAIS +DEFORMEZ +DEFOULANT +DEFOULAT +DEFOULERAI +DEFOULERONT +DEFOURAILLE +DEFOURNA +DEFOURNASSE +DEFOURNEMENT +DEFOURNERENT +DEFOURNEUSES +DEFRAICHIES +DEFRAICHIREZ +DEFRAIEMENTS +DEFRAIERIONS +DEFRANCHIRAI +DEFRAYANT +DEFRAYEE +DEFRAYERENT +DEFRAYONS +DEFRICHAMES +DEFRICHE +DEFRICHERAIS +DEFRICHEUR +DEFRIPAIENT +DEFRIPES +DEFRISAIS +DEFRISAT +DEFRISERAI +DEFRISERONT +DEFROISSES +DEFRONCAMES +DEFRONCE +DEFRONCERAS +DEFRONCIONS +DEFROQUASSE +DEFROQUENT +DEFROQUERIEZ +DEFRUITAI +DEFRUITERAI +DEFRUITERONT +DEFUNTAIS +DEFUNTAT +DEFUNTERAS +DEFUNTIONS +DEFUSIONNANT +DEFUSIONNEES +DEGAGE +DEGAGEASSENT +DEGAGENT +DEGAGEREZ +DEGAINAI +DEGAINASSIEZ +DEGAINERAI +DEGAINERONT +DEGALONNAIT +DEGALONNATES +DEGALONNIEZ +DEGAMMAS +DEGAMMER +DEGAMMERIONS +DEGANTAIENT +DEGANTES +DEGARNIRA +DEGARNIRONS +DEGARNISSENT +DEGASOLINA +DEGASOLINER +DEGAUCHIE +DEGAUCHIRENT +DEGAZAIS +DEGAZAT +DEGAZERAIS +DEGAZEZ +DEGAZOLINEE +DEGAZONNA +DEGAZONNASSE +DEGAZONNONS +DEGELAS +DEGELEES +DEGELEREZ +DEGELS +DEGENASSENT +DEGENER +DEGENERATIVE +DEGENERERAIS +DEGENERONT +DEGERMAIT +DEGERMATES +DEGERMERAIT +DEGERMIEZ +DEGINGANDAS +DEGINGANDENT +DEGIVRAGES +DEGIVRASSES +DEGIVRERA +DEGIVRERONS +DEGLACAGE +DEGLACASSENT +DEGLACEMENTS +DEGLACEREZ +DEGLACIONS +DEGLINGUANT +DEGLINGUEE +DEGLINGUONS +DEGLUASSE +DEGLUENT +DEGLUERIEZ +DEGLUTIE +DEGLUTIRAIT +DEGLUTISSAIS +DEGLUTIT +DEGOBILLAIT +DEGOBILLATES +DEGOBILLIEZ +DEGOISAS +DEGOISEES +DEGOISERAS +DEGOISIONS +DEGOMMANT +DEGOMMEE +DEGOMMERENT +DEGOMMONS +DEGONDASSENT +DEGONDER +DEGONDERIONS +DEGONFLABLES +DEGONFLARDE +DEGONFLATES +DEGONFLES +DEGORGEAIENT +DEGORGEONS +DEGORGERIEZ +DEGOTAIENT +DEGOTASSIONS +DEGOTERAIENT +DEGOTES +DEGOTTAMES +DEGOTTE +DEGOTTERAS +DEGOTTIONS +DEGOUDRONNEZ +DEGOULINAIT +DEGOULINERAI +DEGOUPILLAIT +DEGOUPILLE +DEGOUPILLONS +DEGOURDIS +DEGOURDISSES +DEGOUTAIENT +DEGOUTASSE +DEGOUTEE +DEGOUTERENT +DEGOUTONS +DEGOUTTANTES +DEGOUTTE +DEGOUTTEREZ +DEGRADA +DEGRADAS +DEGRADATION +DEGRADERAIS +DEGRADEZ +DEGRAFANT +DEGRAFEE +DEGRAFERENT +DEGRAFEUSES +DEGRAFFITANT +DEGRAFFITEES +DEGRAFIONS +DEGRAISSANT +DEGRAISSAT +DEGRAISSEUR +DEGRAVOYAMES +DEGRAVOYE +DEGRE +DEGREASSENT +DEGREEMENTS +DEGREEREZ +DEGRES +DEGREVAI +DEGREVASSIEZ +DEGREVER +DEGREVERIONS +DEGRIFFAIENT +DEGRIFFES +DEGRINGOLAI +DEGRINGOLEZ +DEGRIPPANT +DEGRIPPE +DEGRIPPERAS +DEGRIPPIONS +DEGRISASSE +DEGRISEMENT +DEGRISERENT +DEGRISONS +DEGROSSER +DEGROSSIONS +DEGROSSIRIEZ +DEGROUILLANT +DEGROUILLEES +DEGROUPAGE +DEGROUPEREZ +DEGUE +DEGUERPIRA +DEGUERPIRONS +DEGUERPISSES +DEGUEULAI +DEGUEULASSEE +DEGUEULERA +DEGUEULERONS +DEGUILLAI +DEGUILLERAI +DEGUILLERONT +DEGUISAIT +DEGUISATES +DEGUISES +DEGURGITAMES +DEGURGITEZ +DEGUSTANT +DEGUSTATEURS +DEGUSTERA +DEGUSTERONS +DEHALAI +DEHALASSIEZ +DEHALERAI +DEHALERONT +DEHANCHAIT +DEHANCHATES +DEHANCHES +DEHARNACHEE +DEHISCENCE +DEHOTTAIS +DEHOTTAT +DEHOTTERAS +DEHOTTIONS +DEHOUILLASSE +DEHOUILLENT +DEHOUSSABLE +DEHOUSSER +DEICTIQUE +DEIFIASSE +DEIFIEE +DEIFIERENT +DEIFIONS +DEJANTAIENT +DEJANTES +DEJAUGEAIENT +DEJAUGES +DEJAUNIRAI +DEJAUNIRONT +DEJAUNISSIEZ +DEJETAIS +DEJETAT +DEJETIONS +DEJETTEREZ +DEJEUNAIT +DEJEUNATES +DEJEUNERENT +DEJEUNIONS +DEJOUASSE +DEJOUENT +DEJOUERIEZ +DEJUCHAI +DEJUCHASSIEZ +DEJUCHERAI +DEJUCHERONT +DEJUGEAIS +DEJUGEAT +DEJUGERAIS +DEJUGEZ +DELABRAIS +DELABRAT +DELABRERAI +DELABRERONT +DELACA +DELACASSES +DELACERA +DELACERONS +DELAIERA +DELAIERONT +DELAINANT +DELAINEE +DELAINERENT +DELAINONS +DELAISSASSE +DELAISSEMENT +DELAISSERENT +DELAISSONS +DELAITAS +DELAITEES +DELAITERAS +DELAITEZ +DELARDAIT +DELARDATES +DELARDES +DELASSAMES +DELASSERA +DELASSERONS +DELATIONS +DELATTAS +DELATTEES +DELATTEREZ +DELAVA +DELAVASSE +DELAVENT +DELAVERIEZ +DELAYAGE +DELAYASSENT +DELAYER +DELAYERIONS +DELEATUR +DELEATURER +DELECTA +DELECTASSE +DELECTEE +DELECTERENT +DELECTONS +DELEGATIONS +DELEGITIMAS +DELEGITIMEE +DELEGUA +DELEGUASSES +DELEGUERA +DELEGUERONS +DELEMONTAINS +DELESTAS +DELESTEES +DELESTEREZ +DELETERE +DELIANT +DELIASSAIT +DELIASSATES +DELIASSERAIT +DELIASSEUSES +DELIBERAIS +DELIBERASSES +DELIBERERAI +DELIBERERONT +DELICATESSE +DELICIEUX +DELIEES +DELIERAI +DELIERONT +DELIGNAMES +DELIGNE +DELIGNERAS +DELIGNEZ +DELIGNIFIENT +DELIGNURE +DELIMITANT +DELIMITERAIT +DELIMITEURS +DELINEAMENT +DELINEATEUR +DELINEERAI +DELINEERONT +DELINQUANTES +DELIRAI +DELIRASSE +DELIRERA +DELIRERONS +DELISSAGE +DELISSASSENT +DELISSER +DELISSERIONS +DELISTAIENT +DELISTES +DELITAIENT +DELITASSIONS +DELITENT +DELITERIEZ +DELITESCENTS +DELIVRAIT +DELIVRES +DELOCALISAI +DELOCALISERA +DELOGEAIS +DELOGEAT +DELOGERAI +DELOGERONT +DELOGUAMES +DELOGUE +DELOGUERAS +DELOGUIONS +DELOQUASSE +DELOQUENT +DELOQUERIEZ +DELOTS +DELOVASSENT +DELOVER +DELOVERIONS +DELOYALEMENT +DELTACISME +DELTOIDES +DELURAIS +DELURAT +DELURERAIS +DELUREZ +DELUSTRAIT +DELUSTRATES +DELUSTRERAIT +DELUSTRIEZ +DELUTAMES +DELUTE +DELUTERAS +DELUTIONS +DEMAGNETISES +DEMAGOGUE +DEMAIGRIS +DEMAILLAIS +DEMAILLAT +DEMAILLERAIS +DEMAILLEZ +DEMAILLOTANT +DEMAILLOTEES +DEMANCHA +DEMANCHASSES +DEMANCHENT +DEMANCHERIEZ +DEMANDAI +DEMANDASSIEZ +DEMANDERAI +DEMANDERIONS +DEMANDIONS +DEMANGEAMES +DEMANGEE +DEMANGERAS +DEMANGIONS +DEMANTELEREZ +DEMANTIBULA +DEMANTIBULAT +DEMAQUILLAIT +DEMAQUILLEZ +DEMARCATIVE +DEMARCHANT +DEMARCHEE +DEMARCHERENT +DEMARCHEUSES +DEMARIAIS +DEMARIAT +DEMARIERAIS +DEMARIEZ +DEMARQUAIT +DEMARQUATES +DEMARQUERAIT +DEMARQUEURS +DEMARRAI +DEMARRASSIEZ +DEMARRERAI +DEMARRERONT +DEMASCLAGES +DEMASCLASSES +DEMASCLERA +DEMASCLERONS +DEMASQUAIS +DEMASQUAT +DEMASQUERAIS +DEMASQUEZ +DEMASTIQUAT +DEMASTIQUIEZ +DEMATAMES +DEMATE +DEMATERAS +DEMATERIEZ +DEMATINAIS +DEMATINAT +DEMATINERAIS +DEMATINEZ +DEMAZOUTES +DEMELANT +DEMELAT +DEMELERAI +DEMELERONT +DEMELOIRS +DEMEMBRANT +DEMEMBREE +DEMEMBRERAIT +DEMEMBRIEZ +DEMENAGEAMES +DEMENAGEE +DEMENAGERAIS +DEMENAGEUR +DEMENAIT +DEMENATES +DEMENERAIENT +DEMENES +DEMENTANT +DEMENTIELS +DEMENTIRAIT +DEMENTISSENT +DEMERDAIENT +DEMERDASSE +DEMERDENT +DEMERDERIEZ +DEMERDIEZ +DEMERITAS +DEMERITER +DEMERSALES +DEMETTAIENT +DEMETTONS +DEMETTRIONS +DEMEUBLAS +DEMEUBLEES +DEMEUBLEREZ +DEMEURA +DEMEURASSES +DEMEURERA +DEMEURERONS +DEMIE +DEMIELLER +DEMINAIT +DEMINATES +DEMINERAIT +DEMINERONS +DEMIS +DEMISSIONNAT +DEMIXTIONS +DEMOBILISANT +DEMOBILISEZ +DEMOCRATISA +DEMOCRATISAT +DEMODAS +DEMODEES +DEMODEREZ +DEMODONS +DEMODULE +DEMODULERAS +DEMODULIONS +DEMOISELLE +DEMOLIRAIS +DEMOLISSAGE +DEMOLISSEURS +DEMOLITIONS +DEMONETISANT +DEMONETISE +DEMONETISONS +DEMONISAS +DEMONISEES +DEMONISEREZ +DEMONISMES +DEMONTAGE +DEMONTANTS +DEMONTEE +DEMONTERENT +DEMONTEUSES +DEMONTRAI +DEMONTRERAI +DEMONTRERONT +DEMORALISAIT +DEMORALISEES +DEMORDAIENT +DEMORDIONS +DEMORDONS +DEMORDRIONS +DEMOTIVAIENT +DEMOTIVEES +DEMOTIVEREZ +DEMOUCHETA +DEMOUCHETEZ +DEMOULAIENT +DEMOULES +DEMOUSTIQUAI +DEMULTIPLIEE +DEMUNIE +DEMUNIRENT +DEMUNISSANT +DEMUSELA +DEMUSELASSES +DEMUSELES +DEMUTISAI +DEMUTISER +DEMYSTIFIE +DEMYSTIFIONS +DEMYTHIFIER +DENANTIMES +DENANTIRIEZ +DENANTISSENT +DENASALISA +DENASALISER +DENATTAIT +DENATTATES +DENATTERAIT +DENATTIEZ +DENATURASSES +DENATURENT +DENATURERIEZ +DENAZIFIAI +DENAZIFIER +DENDRITIQUE +DENDROLAGUE +DENDROMETRE +DENEBULAIT +DENEBULATES +DENEBULERA +DENEBULERONS +DENEBULISAIT +DENEBULISIEZ +DENEIGEAI +DENEIGER +DENEIGERIONS +DENERVAI +DENERVASSIEZ +DENERVER +DENERVERIONS +DENI +DENIAISAMES +DENIAISE +DENIAISERAIS +DENIAISEZ +DENIASSES +DENICHAIS +DENICHAT +DENICHERAIS +DENICHEUR +DENIERAI +DENIERONT +DENIGRANT +DENIGRAT +DENIGRERAI +DENIGRERONT +DENIIEZ +DENITRAIT +DENITRES +DENIVELANT +DENIVELEE +DENIVELLE +DENIVELLEREZ +DENOIERAI +DENOIES +DENOMBRAS +DENOMBREES +DENOMBRERAS +DENOMBRIONS +DENOMMA +DENOMMASSES +DENOMMERA +DENOMMERONS +DENONCAIS +DENONCAT +DENONCERAIS +DENONCEZ +DENOTA +DENOTASSES +DENOTATIVES +DENOTERAIT +DENOTIEZ +DENOUAS +DENOUEES +DENOUERAS +DENOUIONS +DENOYANT +DENOYAUTAGE +DENOYAUTER +DENOYE +DENREE +DENSIFIAMES +DENSIFIERAIS +DENSIFIEZ +DENSITE +DENTAIRE +DENTASSE +DENTEES +DENTELAS +DENTELEES +DENTELIONS +DENTELLERIE +DENTELLIERS +DENTERAIT +DENTICULE +DENTINAIRES +DENTITION +DENUAIT +DENUATES +DENUDAMES +DENUDATION +DENUDERAIS +DENUDEZ +DENUER +DENUERIONS +DENUTRIES +DEONTIQUES +DEPACSAIENT +DEPACSES +DEPAILLAIS +DEPAILLAT +DEPAILLERAIS +DEPAILLEZ +DEPALISSAIT +DEPALISSATES +DEPALISSIEZ +DEPANNAMES +DEPANNE +DEPANNERAS +DEPANNEUSE +DEPAQUETIEZ +DEPARAIENT +DEPARASITAT +DEPARASITIEZ +DEPARE +DEPAREILLAS +DEPAREILLENT +DEPARERA +DEPARERONS +DEPARIANT +DEPARIEE +DEPARIERENT +DEPARIONS +DEPARLASSENT +DEPARLERAI +DEPARLERONT +DEPARTAGEA +DEPARTAGERAI +DEPARTEMENT +DEPARTIE +DEPARTIRAIT +DEPARTISSAIS +DEPARTIT +DEPASSAIENT +DEPASSASSIEZ +DEPASSER +DEPASSERIONS +DEPATOUILLA +DEPATOUILLAT +DEPATRIASSE +DEPATRIENT +DEPATRIERIEZ +DEPAVAGE +DEPAVASSENT +DEPAVER +DEPAVERIONS +DEPAYSAIENT +DEPAYSASSENT +DEPAYSEMENTS +DEPAYSEREZ +DEPECA +DEPECASSE +DEPECEMENT +DEPECERENT +DEPECEUSES +DEPECHASSE +DEPECHENT +DEPECHERIEZ +DEPECIONS +DEPEIGNASSE +DEPEIGNENT +DEPEIGNERIEZ +DEPEIGNIS +DEPEINDRAI +DEPEINDRONT +DEPENALISA +DEPENALISER +DEPENDAIENT +DEPENDE +DEPENDIONS +DEPENDONS +DEPENDRIONS +DEPENSAI +DEPENSASSIEZ +DEPENSERAI +DEPENSERONT +DEPERDITION +DEPERIRAS +DEPERISSAIT +DEPERISSEZ +DEPERLANTES +DEPETRASSENT +DEPETRER +DEPETRERIONS +DEPEUPLAIENT +DEPEUPLERA +DEPEUPLERONS +DEPHASAI +DEPHASASSIEZ +DEPHASERAI +DEPHASERONT +DEPHOSPHOREZ +DEPIAUTAIT +DEPIAUTATES +DEPIAUTERAIT +DEPIAUTIEZ +DEPIGMENTAIS +DEPIGMENTEZ +DEPILAIT +DEPILATES +DEPILENT +DEPILERIEZ +DEPIQUAGE +DEPIQUASSENT +DEPIQUER +DEPIQUERIONS +DEPISTABLES +DEPISTASSE +DEPISTENT +DEPISTERIEZ +DEPISTONS +DEPITASSE +DEPITENT +DEPITERIEZ +DEPLACA +DEPLACASSES +DEPLACENT +DEPLACERIEZ +DEPLAFONNAI +DEPLAFONNERA +DEPLAIRAIT +DEPLAISAIS +DEPLAISEZ +DEPLANAIS +DEPLANAT +DEPLANERAIS +DEPLANEZ +DEPLANTAIS +DEPLANTAT +DEPLANTERAI +DEPLANTERONT +DEPLANTINANT +DEPLANTINEES +DEPLANTOIR +DEPLATRAMES +DEPLATRE +DEPLATRERAS +DEPLATRIONS +DEPLIAIT +DEPLIASSIEZ +DEPLIER +DEPLIERIONS +DEPLISSAGES +DEPLISSASSES +DEPLISSERA +DEPLISSERONS +DEPLOIENT +DEPLOIERONS +DEPLOMBAMES +DEPLOMBE +DEPLOMBERAS +DEPLOMBIONS +DEPLORAMES +DEPLORATION +DEPLORERAIS +DEPLOREZ +DEPLOYAIT +DEPLOYATES +DEPLOYONS +DEPLUMASSE +DEPLUMENT +DEPLUMERIEZ +DEPLUS +DEPOETISES +DEPOINTAIS +DEPOINTAT +DEPOINTERAIS +DEPOINTEZ +DEPOLARISENT +DEPOLIES +DEPOLIREZ +DEPOLISSAIT +DEPOLISSONS +DEPOLITISAS +DEPOLITISEE +DEPOLLUA +DEPOLLUAS +DEPOLLUEES +DEPOLLUEREZ +DEPOLLUIONS +DEPONENTES +DEPORTAMES +DEPORTAT +DEPORTER +DEPORTERIONS +DEPOSAI +DEPOSASSE +DEPOSENT +DEPOSERIEZ +DEPOSITION +DEPOSSEDAMES +DEPOSSEDE +DEPOSSEDERAS +DEPOSSEDIONS +DEPOTAIS +DEPOTAT +DEPOTERAI +DEPOTERONT +DEPOUDRAI +DEPOUDRERAI +DEPOUDRERONT +DEPOUILLERA +DEPOURVUS +DEPOUSSIERER +DEPRAVAIENT +DEPRAVASSENT +DEPRAVATRICE +DEPRAVERAIS +DEPRAVEZ +DEPRECIA +DEPRECIASSES +DEPRECIERA +DEPRECIERONS +DEPREDATIONS +DEPRENDS +DEPRESSIF +DEPRIMANTS +DEPRIMEE +DEPRIMERENT +DEPRIMONS +DEPRISAS +DEPRISEES +DEPRISEREZ +DEPRISSE +DEPROGRAMME +DEPROTEGEAT +DEPROTEGEZ +DEPUCELAMES +DEPUCELE +DEPUCELLENT +DEPULPAIS +DEPULPAT +DEPULPERAIS +DEPULPEZ +DEPURANT +DEPURATIFS +DEPURERA +DEPURERONS +DEPUTAIS +DEPUTAT +DEPUTERAI +DEPUTERONT +DEQUALIFIAIT +DEQUILLASSE +DEQUILLENT +DEQUILLERIEZ +DERACINA +DERACINASSE +DERACINEMENT +DERACINERENT +DERACINONS +DERADASSENT +DERADERAI +DERADERONT +DERAGEAIS +DERAGEAT +DERAGERAS +DERAGIONS +DERAIDIRAIT +DERAIDISSAIS +DERAIDIT +DERAIEREZ +DERAILLAIT +DERAILLATES +DERAILLERAIT +DERAILLEURS +DERAISONNAT +DERAISONNIEZ +DERAMANT +DERAMEE +DERAMERENT +DERAMONS +DERANGEANTES +DERANGEE +DERANGERAIS +DERANGEZ +DERAPAMES +DERAPE +DERAPEREZ +DERASA +DERASASSES +DERASENT +DERASERIEZ +DERATAI +DERATASSIEZ +DERATERAI +DERATERONT +DERATISAMES +DERATISATION +DERATISERAIS +DERATISEUR +DERAYAGE +DERAYASSENT +DERAYER +DERAYERIONS +DERAYURE +DEREALISAI +DEREALISASSE +DEREALISEE +DEREALISONS +DEREGLAI +DEREGLASSIEZ +DEREGLERAS +DEREGLIONS +DEREGULASSE +DEREGULEE +DEREGULERENT +DEREGULONS +DERIDAGES +DERIDASSES +DERIDERA +DERIDERONS +DERIVAIT +DERIVASSIEZ +DERIVE +DERIVERAS +DERIVETAIENT +DERIVETIEZ +DERIVETTERAS +DERIVIEZ +DERMATOS +DERMIQUES +DERNIER +DEROBAI +DEROBASSIEZ +DEROBER +DEROBERIONS +DEROBIONS +DEROCHANT +DEROCHEE +DEROCHERAIT +DEROCHIEZ +DERODAMES +DERODE +DERODERAS +DERODIONS +DEROGEAIS +DEROGEASSIEZ +DEROGERAIS +DEROGEZ +DEROQUAS +DEROQUEES +DEROQUEREZ +DEROUGI +DEROUGIRAS +DEROUGISSAIT +DEROUGITES +DEROUILLAS +DEROUILLEES +DEROUILLEREZ +DEROULA +DEROULANTES +DEROULE +DEROULERAIS +DEROULEUR +DEROUTAGES +DEROUTAS +DEROUTEES +DEROUTERAS +DEROUTIONS +DERUPITAI +DERUPITERAIS +DERUPITEZ +DESABONNAI +DESABONNER +DESABUSAIENT +DESABUSERA +DESABUSERONS +DESACCENTUE +DESACCLIMATA +DESACCLIMATE +DESACCORDEE +DESACCORDS +DESACCOUPLEZ +DESACIDIFIAI +DESACIERER +DESACTIVEES +DESACTIVEREZ +DESADAPTA +DESADAPTENT +DESAERAGE +DESAERASSENT +DESAEREES +DESAEREREZ +DESAFFECTA +DESAFFECTER +DESAFFILIAIS +DESAFFILIEZ +DESAGRAFANT +DESAGRAFEE +DESAGRAFONS +DESAGREGEAIS +DESAGREGIEZ +DESAIMANTANT +DESAIMANTE +DESAIMANTONS +DESAJUSTA +DESAJUSTENT +DESALIENAI +DESALIENER +DESALIGNERA +DESALINISAIS +DESALINISEZ +DESALPANT +DESALPENT +DESALPERIEZ +DESALTERAI +DESALTERASSE +DESALTERENT +DESAMAI +DESAMASSIEZ +DESAMEES +DESAMEREZ +DESAMIANTAI +DESAMIANTEZ +DESAMINAI +DESAMINEES +DESAMINEREZ +DESAMIONS +DESAMORCANT +DESAMORCEE +DESAMORCONS +DESANGOISSAS +DESANGOISSES +DESANNEXAMES +DESANNEXE +DESANNEXERAS +DESANNEXION +DESANNONCAS +DESANNONCENT +DESAPAIENT +DESAPASSIONS +DESAPERAIENT +DESAPES +DESAPPARIEE +DESAPPOINTA +DESAPPOINTAT +DESAPPRENEZ +DESAPPRISE +DESAPPROUVER +DESARCONNAIT +DESARCONNEZ +DESARETANT +DESARETEE +DESARETERENT +DESARETONS +DESARGENTERA +DESARMAIT +DESARMASSIEZ +DESARMER +DESARMERIONS +DESARRIMAGES +DESARRIMERA +DESARTICULAI +DESASSEMBLER +DESASSIMILEE +DESASSORTIE +DESASTRES +DESATOMISAT +DESATOMISES +DESAVANTAGER +DESAVOUA +DESAVOUASSES +DESAVOUERA +DESAVOUERONS +DESAXAIS +DESAXAT +DESAXERAIS +DESAXEZ +DESCELLANT +DESCELLEE +DESCELLERAIT +DESCELLIEZ +DESCENDANTE +DESCENDEUSE +DESCENDISSES +DESCENDRAIT +DESCENDUE +DESCOLARISAI +DESCRIPTIVE +DESECHOUAGES +DESECHOUERA +DESEMBALLAS +DESEMBALLENT +DESEMBOBINE +DESEMBOURBA +DESEMBOURBAT +DESEMBUAMES +DESEMBUE +DESEMBUERAS +DESEMBUIONS +DESEMPARASSE +DESEMPARENT +DESEMPESAI +DESEMPESERAI +DESEMPLIR +DESEMPLISSES +DESENCADRANT +DESENCADREES +DESENCADRONS +DESENCHANTAS +DESENCHANTER +DESENCLAVAIT +DESENCLAVE +DESENCLAVIEZ +DESENCOLLEE +DESENCOMBRA +DESENCOMBRAT +DESENCRAMES +DESENCRASSAS +DESENCRASSES +DESENCRENT +DESENCRERIEZ +DESENDETTAI +DESENDETTERA +DESENERVAIT +DESENERVATES +DESENERVIEZ +DESENFILAS +DESENFILEES +DESENFILEREZ +DESENFLA +DESENFLASSE +DESENFLENT +DESENFLERIEZ +DESENFLURES +DESENFUMAMES +DESENFUME +DESENFUMERAS +DESENFUMIONS +DESENGAGEAS +DESENGLUA +DESENGLUERA +DESENGORGEE +DESENGORGIEZ +DESENGRENEE +DESENIVRA +DESENIVRERA +DESENLACAIS +DESENLACAT +DESENLACEZ +DESENLAIDIS +DESENLAIDIT +DESENNUIEREZ +DESENNUYAIT +DESENNUYATES +DESENNUYONS +DESENRAYAMES +DESENRAYE +DESENRAYERAS +DESENRAYIONS +DESENSIMES +DESENTOILES +DESENTRAVER +DESENVASAIS +DESENVASAT +DESENVASEZ +DESENVENIME +DESENVERGUA +DESENVERGUAT +DESEPAISSIE +DESEQUIPAMES +DESEQUIPE +DESEQUIPERAS +DESEQUIPIONS +DESERTAS +DESERTEES +DESERTEREZ +DESERTIFIA +DESERTIFIERA +DESESPERAIT +DESESPERASSE +DESESPEREREZ +DESESPOIR +DESETAMANT +DESETAMEE +DESETAMERENT +DESETAMONS +DESETATISENT +DESEXCITERA +DESEXUALISE +DESHABILLA +DESHABILLER +DESHABITUAIS +DESHERBANT +DESHERBAT +DESHERBERAIS +DESHERBEZ +DESHERITAIT +DESHERITATES +DESHERITES +DESHONNEUR +DESHONORE +DESHONORERAS +DESHONORIONS +DESHUILANT +DESHUILEE +DESHUILERENT +DESHUILIEZ +DESHUMANISEE +DESHYDRATIEZ +DESIDERATUMS +DESIGNASSE +DESIGNATIONS +DESIGNERS +DESIGNS +DESILASSENT +DESILER +DESILERIONS +DESINCARNAS +DESINCARNEE +DESINCORPORA +DESINCORPORE +DESINCRUSTE +DESINDEXA +DESINDEXENT +DESINENCES +DESINFECTAT +DESINFORMAT +DESINFORMENT +DESINHIBER +DESINSCRITE +DESINSCRIVEZ +DESINSERAMES +DESINSERE +DESINSERERAS +DESINSERIONS +DESINTEGRAIS +DESINTEGREZ +DESINVESTIRA +DESINVITAS +DESINVITEES +DESINVITEREZ +DESINVOLTE +DESIRAIS +DESIRASSES +DESIRERA +DESIRERONS +DESIRS +DESISTASSENT +DESISTEMENTS +DESISTEREZ +DESK +DESMOTROPIE +DESOBEIRAIS +DESOBEISSENT +DESOBLIGEAI +DESOBLIGEES +DESOBSTRUENT +DESOCCUPEES +DESOCIALISES +DESODORISAI +DESODORISEES +DESOEUVREE +DESOLAMES +DESOLASSIONS +DESOLERA +DESOLERONS +DESOLONS +DESOPERCULEZ +DESOPILANT +DESOPILAT +DESOPILERAIS +DESOPILEZ +DESORBITANT +DESORBITIEZ +DESORDONNAS +DESORDONNENT +DESORGANISA +DESORGANISAT +DESORGANISEZ +DESORIENTANT +DESORIENTE +DESORIENTONS +DESOSSAIT +DESOSSATES +DESOSSES +DESOXYDAMES +DESOXYDERA +DESOXYDERONS +DESOXYGENAIS +DESOXYGENEZ +DESPOTE +DESQUAMAIS +DESQUAMASSES +DESQUAMENT +DESQUAMERIEZ +DESQUELS +DESSABLAS +DESSABLEES +DESSABLERAS +DESSABLIONS +DESSAISIRAIS +DESSALAISONS +DESSALAT +DESSALERAI +DESSALERONT +DESSALURES +DESSANGLER +DESSAOULES +DESSAPAMES +DESSAPE +DESSAPERAS +DESSAPIONS +DESSECHANTES +DESSECHE +DESSECHERAIS +DESSECHEZ +DESSELLAIT +DESSELLATES +DESSELLERAIT +DESSELLIEZ +DESSERRAMES +DESSERRE +DESSERRERAIS +DESSERREZ +DESSERTIES +DESSERTIREZ +DESSERTITES +DESSERVEZ +DESSERVIS +DESSEVAGES +DESSILLAI +DESSILLERAI +DESSILLERONT +DESSINAIS +DESSINAT +DESSINER +DESSINERIONS +DESSOLAI +DESSOLASSIEZ +DESSOLER +DESSOLERIONS +DESSOUCHAGES +DESSOUCHERA +DESSOUDAIS +DESSOUDAT +DESSOUDERAIS +DESSOUDEZ +DESSOULAIT +DESSOULATES +DESSOULERAIT +DESSOULIEZ +DESSUINTAIT +DESSUINTATES +DESSUINTIEZ +DESTALINISEZ +DESTINAMES +DESTINER +DESTINERIONS +DESTITUABLE +DESTITUER +DESTOCKA +DESTOCKASSE +DESTOCKENT +DESTOCKERIEZ +DESTOCKONS +DESTRESSAMES +DESTRESSES +DESTRUCTEUR +DESTRUCTURE +DESUETE +DESULFITAIS +DESULFITAT +DESULFITEZ +DESULFURANT +DESULFURIEZ +DESUNIRA +DESUNIRONS +DESUNISSEZ +DETACHAGES +DETACHAS +DETACHEES +DETACHERAS +DETACHEUSE +DETAILLAIS +DETAILLASSES +DETAILLERA +DETAILLERONS +DETALAIENT +DETALASSIONS +DETALERAIT +DETALIEZ +DETALONNANT +DETALONNEE +DETALONNONS +DETAPISSASSE +DETAPISSENT +DETARTRAGE +DETARTRANTS +DETARTREE +DETARTRERENT +DETARTRIEZ +DETASSAS +DETASSEES +DETASSEREZ +DETATOUA +DETATOUASSE +DETATOUENT +DETATOUERIEZ +DETAXAI +DETAXASSIEZ +DETAXER +DETAXERIONS +DETECTABLES +DETECTASSES +DETECTERA +DETECTERONS +DETECTIVES +DETEIGNES +DETEINDRAS +DETEINTES +DETELANT +DETELEE +DETELLERA +DETELLERONT +DETENDAIT +DETENDIONS +DETENDONS +DETENDRIONS +DETENIONS +DETENTRICES +DETERGEAIT +DETERGEATES +DETERGER +DETERGERIONS +DETERIORAIS +DETERIORAT +DETERIORERAI +DETERMINEZ +DETERRAGES +DETERRASSES +DETERRENT +DETERRERIEZ +DETERRIEZ +DETESTABLE +DETESTASSE +DETESTEE +DETESTERENT +DETESTONS +DETHEINER +DETIENNENT +DETINSSIEZ +DETIRANT +DETIREE +DETIRERENT +DETIRIEZ +DETONANTE +DETONATES +DETONERAIENT +DETONES +DETONNAIT +DETONNATES +DETONNERENT +DETONNONS +DETORDEZ +DETORDRE +DETORDUS +DETORTILLAIT +DETORTILLE +DETORTILLONS +DETOURANT +DETOUREE +DETOURERENT +DETOURNA +DETOURNASSES +DETOURNENT +DETOURNERIEZ +DETOURS +DETOXIFIAS +DETOXIFIE +DETOXIFIERAS +DETOXIFIIONS +DETOXIQUASSE +DETOXIQUENT +DETRACTAI +DETRACTERAI +DETRACTERONT +DETRACTRICES +DETRAQUEREZ +DETREMPA +DETREMPASSES +DETREMPERA +DETREMPERONS +DETRICOTAGE +DETRICOTER +DETRITIQUE +DETROMPAIS +DETROMPAT +DETROMPERAIS +DETROMPEUR +DETRONAIT +DETRONATES +DETRONERAIT +DETRONIEZ +DETROQUAMES +DETROQUE +DETROQUERAS +DETROQUIONS +DETROUSSASSE +DETRUIRAS +DETRUISAIT +DETRUISIS +DETRUITE +DEUILS +DEUTERON +DEUXIEMEMENT +DEVALAIENT +DEVALASSIONS +DEVALERAIENT +DEVALES +DEVALISANT +DEVALISEE +DEVALISERENT +DEVALISEUSES +DEVALORISENT +DEVALUAIENT +DEVALUERA +DEVALUERONS +DEVANCAI +DEVANCASSIEZ +DEVANCER +DEVANCERIONS +DEVANCIONS +DEVARIAIT +DEVARIATES +DEVARIERAIT +DEVARIIEZ +DEVASAS +DEVASEES +DEVASERAS +DEVASIONS +DEVASTASSE +DEVASTATIONS +DEVASTES +DEVELOPPAI +DEVELOPPEES +DEVELOPPEURS +DEVENANT +DEVENTAIS +DEVENTAT +DEVENTERAIS +DEVENTEZ +DEVERBALES +DEVERDIR +DEVERDIRIONS +DEVERDISSES +DEVERGONDAI +DEVERGONDEZ +DEVERGUANT +DEVERGUEE +DEVERGUERENT +DEVERGUONS +DEVERNIRAIT +DEVERS +DEVERSASSENT +DEVERSEMENTS +DEVERSEREZ +DEVERSOIRS +DEVETENT +DEVETIRAIS +DEVETISSE +DEVETUE +DEVIAMES +DEVIASSES +DEVIDAIS +DEVIDAT +DEVIDERAIS +DEVIDEUR +DEVIE +DEVIENDRIEZ +DEVIERA +DEVIERGEAI +DEVIERGERAI +DEVIERGERONT +DEVIEZ +DEVINAIT +DEVINATES +DEVINERAIT +DEVINES +DEVINMES +DEVINTES +DEVIRASSE +DEVIRENT +DEVIRERIEZ +DEVIRILISAI +DEVIRILISERA +DEVISAGE +DEVISAGER +DEVISAIT +DEVISATES +DEVISERAIT +DEVISIEZ +DEVISSAIS +DEVISSAT +DEVISSERAIS +DEVISSEZ +DEVITALISANT +DEVITALISE +DEVITALISONS +DEVITRIFIE +DEVITRIFIONS +DEVOIERAS +DEVOILAIS +DEVOILAT +DEVOILERAI +DEVOILERONT +DEVOISEES +DEVOLTANT +DEVOLTEE +DEVOLTERENT +DEVOLTIEZ +DEVOLUTIONS +DEVORAI +DEVORASSE +DEVORATRICES +DEVORERAIT +DEVOREURS +DEVOTES +DEVOTS +DEVOUASSENT +DEVOUEMENTS +DEVOUEREZ +DEVOYA +DEVOYASSES +DEVOYES +DEVRAS +DEVRILLAMES +DEVRILLE +DEVRILLERAS +DEVRILLIONS +DEWATTES +DEXTRORSE +DEZINGUAIENT +DEZINGUES +DEZIPPAMES +DEZIPPE +DEZIPPERAS +DEZIPPIONS +DEZONANT +DEZONEE +DEZONERENT +DEZONONS +DEZOOMASSENT +DEZOOMER +DEZOOMERIONS +DHIMMA +DIABETIQUES +DIABLERIES +DIABOLIQUES +DIABOLISEES +DIABOLISEREZ +DIABOLO +DIACHYLUM +DIACONALES +DIACRITIQUE +DIAGNOSTIC +DIAL +DIALECTAUX +DIALECTISA +DIALECTISAS +DIALECTISENT +DIALOGUAI +DIALOGUERAI +DIALOGUERONT +DIALYPETALES +DIALYSASSENT +DIALYSEPALE +DIALYSEREZ +DIALYSIONS +DIAMANTAIENT +DIAMANTASSES +DIAMANTERA +DIAMANTERONS +DIAMANTINS +DIAMETRES +DIAMORPHINES +DIAPEDESE +DIAPHORESE +DIAPHRAGMANT +DIAPHRAGMEE +DIAPHYSAIRE +DIAPOSITIVE +DIAPRASSE +DIAPRENT +DIAPRERIEZ +DIAPRURES +DIASCOPE +DIASTASE +DIATHERMANE +DIATHERMIQUE +DIATOMITE +DIAULES +DIBASIQUES +DICARBONYLES +DICETONES +DICHOTOMES +DICHROMATES +DICOS +DICTAIS +DICTASSES +DICTATORIAUX +DICTERA +DICTERONS +DIDACTICIEL +DIDACTISMES +DIDUCTIONS +DIEGETIQUES +DIENES +DIESA +DIESASSES +DIESELISAI +DIESELISER +DIESELS +DIESEREZ +DIESTER +DIETETIQUES +DIFFAMAIENT +DIFFAMASSENT +DIFFAMATOIRE +DIFFAMERAI +DIFFAMERONT +DIFFERAIS +DIFFERAT +DIFFERENTIAI +DIFFERERAI +DIFFERERONT +DIFFICULTES +DIFFLUANT +DIFFLUENCE +DIFFLUERAIS +DIFFLUEZ +DIFFRACTES +DIFFUS +DIFFUSANTE +DIFFUSATES +DIFFUSERAIS +DIFFUSEUR +DIGERA +DIGERASSES +DIGERERA +DIGERERONS +DIGESTEUR +DIGESTIVE +DIGITALINES +DIGITALISAS +DIGITALISEE +DIGITIGRADES +DIGNE +DIGON +DIGRESSAIT +DIGRESSATES +DIGRESSERENT +DIGRESSIFS +DIHYDROGENE +DILACERAI +DILACERER +DILAPIDAIENT +DILAPIDEE +DILAPIDERENT +DILAPIDONS +DILATAMES +DILATASSIONS +DILATEE +DILATERENT +DILATIONS +DILEMMES +DILIGENTAI +DILIGENTERAI +DILUAIS +DILUASSES +DILUERA +DILUERONS +DILUTIFS +DILUVIENNE +DIMENSIONNAI +DIMENSIONNER +DIMETRODON +DIMINUASSE +DIMINUENDO +DIMINUERENT +DIMINUONS +DIMISSORIALE +DINAIENT +DINANT +DINATES +DINDONNAIT +DINDONNATES +DINDONNES +DINE +DINEREZ +DINEURS +DINGHYS +DINGUAS +DINGUER +DINGUERIES +DINIEZ +DINORNIS +DIOCESAINE +DIOECIES +DIONEES +DIOPTRIE +DIOT +DIPETALE +DIPHTERIE +DIPHTONGUAIT +DIPHTONGUE +DIPHTONGUONS +DIPLOIDIE +DIPLOMANTES +DIPLOMATES +DIPLOMERA +DIPLOMERONS +DIPLOPIES +DIPNEUSTES +DIPSACEES +DIPTYQUES +DIRECT +DIRECTOIRE +DIRECTS +DIRIGEA +DIRIGEANTES +DIRIGEE +DIRIGERAS +DIRIGIONS +DIRLO +DISAIS +DISCAUX +DISCERNAS +DISCERNEES +DISCERNERAS +DISCERNIONS +DISCIPLINAT +DISCIPLINIEZ +DISCOIDAL +DISCOMPTAIT +DISCOMPTATES +DISCOMPTEURS +DISCONTINUA +DISCONTINUAT +DISCONVENUS +DISCONVIENNE +DISCOPHILIES +DISCORDANT +DISCORDAT +DISCORDERAS +DISCORDIONS +DISCOUNTAI +DISCOUNTERAI +DISCOURAIENT +DISCOUREUSES +DISCOURRAIT +DISCOURTOISE +DISCOURUSSES +DISCREDITAIT +DISCREDITE +DISCREDITONS +DISCRETISAI +DISCRETISERA +DISCRIMINAI +DISCRIMINERA +DISCULPAIT +DISCULPATES +DISCULPES +DISCUSSION +DISCUTASSES +DISCUTERA +DISCUTERONS +DISCUTONS +DISETTES +DISGRACIA +DISGRACIERA +DISHARMONIE +DISJOIGNES +DISJOINDRAS +DISJOINTES +DISJONCTASSE +DISJONCTENT +DISJONCTIFS +DISLOQUAIENT +DISLOQUES +DISPARAIT +DISPARITIONS +DISPARUSSIEZ +DISPATCHANT +DISPATCHEE +DISPATCHEUSE +DISPENDIEUX +DISPENSAMES +DISPENSATEUR +DISPENSER +DISPERSAIENT +DISPERSAS +DISPERSEE +DISPERSERAIT +DISPERSIEZ +DISPO +DISPOSAIT +DISPOSASSIEZ +DISPOSERAI +DISPOSERONT +DISPUTAILLE +DISPUTAMES +DISPUTE +DISPUTERAS +DISPUTIONS +DISQUALIFIAI +DISQUATES +DISQUERAIT +DISQUETTES +DISRUPTIONS +DISSECTION +DISSEMINAIT +DISSEMINATES +DISSEMINES +DISSEQUA +DISSEQUASSES +DISSEQUERA +DISSEQUERONS +DISSEQUONS +DISSERTASSE +DISSERTENT +DISSERTERIEZ +DISSES +DISSIMULASSE +DISSIMULES +DISSIPAIT +DISSIPATES +DISSIPERAIT +DISSIPIEZ +DISSOCIAIS +DISSOCIAT +DISSOCIERAI +DISSOCIERONT +DISSOLUBLES +DISSOLVAIS +DISSOLVIEZ +DISSONANCES +DISSONERAIT +DISSONIEZ +DISSOUDREZ +DISSUADAI +DISSUADERAI +DISSUADERONT +DISSUASIVE +DISTAL +DISTANCAS +DISTANCEES +DISTANCERAIS +DISTANCEZ +DISTANCIEES +DISTANCIEREZ +DISTANCONS +DISTENDANT +DISTENDISSE +DISTENDS +DISTILLAI +DISTILLATS +DISTILLERAIT +DISTILLES +DISTINCTIFS +DISTINGUES +DISTOMATOSES +DISTORDES +DISTORDRAS +DISTORDUES +DISTRACTIONS +DISTRAIS +DISTRAYANTE +DISTRIBUAI +DISTRIBUERAI +DISTRIBUTIF +DITE +DIURETIQUE +DIVAGATIONS +DIVAGUAS +DIVAGUER +DIVAGUERIONS +DIVALENT +DIVERGEAI +DIVERGEONS +DIVERGERIEZ +DIVERSEMENT +DIVERSIFIENT +DIVERSION +DIVERTIES +DIVERTIRAIT +DIVERTISSAIS +DIVERTISSES +DIVIDENDE +DIVINATRICES +DIVINISANT +DIVINISERAIT +DIVINISIEZ +DIVISAIS +DIVISAT +DIVISERAIS +DIVISEUR +DIVISION +DIVORCAI +DIVORCASSIEZ +DIVORCERAI +DIVORCERONT +DIVULGATION +DIVULGUANT +DIVULGUEE +DIVULGUERENT +DIVULGUONS +DIXIEMEMENT +DIZENIER +DJAINISMES +DJEMBE +DJIBOUTIEN +DJINN +DOCETISME +DOCIMOLOGIES +DOCTES +DOCTORANTE +DOCTRINAIRES +DOCU +DOCUMENTASSE +DOCUMENTEE +DOCUMENTONS +DODECAGONAUX +DODECASTYLES +DODELINAS +DODELINEES +DODELINERAS +DODELINIONS +DODINANT +DODINEE +DODINERENT +DODINONS +DOGARESSE +DOGMATIQUES +DOGMATISERAI +DOGMATISTE +DOGUINES +DOIGTAS +DOIGTEES +DOIGTEREZ +DOIGTIONS +DOIVES +DOLAIT +DOLATES +DOLEAUX +DOLERAIENT +DOLES +DOLINES +DOLLARISAMES +DOLLARISEZ +DOLOIRE +DOLOMITES +DOLOS +DOMAINES +DOMES +DOMESTIQUAIS +DOMICILIAIT +DOMICILIERA +DOMIENS +DOMINANTE +DOMINATES +DOMINENT +DOMINERIEZ +DOMINICAINE +DOMINIQUAIS +DOMINOTIERES +DOMPTA +DOMPTANT +DOMPTEE +DOMPTERENT +DOMPTEUSES +DONAS +DONATISTES +DONDONS +DONJUANISA +DONJUANISIEZ +DONNAMES +DONNASSIONS +DONNERAIENT +DONNES +DONS +DOPAI +DOPANTE +DOPATES +DOPERAIT +DOPEURS +DOPPLERS +DORAIT +DORATES +DORERA +DORERONS +DORIENNES +DORLOTAI +DORLOTASSIEZ +DORLOTER +DORLOTERIONS +DORMAIT +DORMEUR +DORMIONS +DORMIRIEZ +DORMIT +DORONICS +DOSABLE +DOSAS +DOSEES +DOSEREZ +DOSEUSE +DOSIONS +DOSSIERES +DOTAL +DOTASSES +DOTEES +DOTEREZ +DOTS +DOUAISIENNE +DOUANCE +DOUARS +DOUBIENNE +DOUBLAIS +DOUBLASSES +DOUBLEMENT +DOUBLERENT +DOUBLETTE +DOUBLIONS +DOUBLONNAS +DOUBLONNER +DOUBLURES +DOUCEURS +DOUCHANBEENS +DOUCHE +DOUCHERAS +DOUCHEUR +DOUCI +DOUCIRAI +DOUCIRONT +DOUCISSES +DOUDOUNE +DOUERA +DOUERONS +DOUILLAIENT +DOUILLES +DOUILLIONS +DOUM +DOUROS +DOUTAS +DOUTEES +DOUTEREZ +DOUTEUSES +DOUVELLES +DOUZIEMES +DOYENS +DRACHAT +DRACONIENNES +DRAGEIFIAI +DRAGEIFIERAI +DRAGEONNAGE +DRAGEONNER +DRAGLINES +DRAGSTER +DRAGUASSE +DRAGUENT +DRAGUERIEZ +DRAGUIEZ +DRAIERAS +DRAINA +DRAINANTES +DRAINE +DRAINERAS +DRAINEUSE +DRAISINES +DRAMATISAI +DRAMATISASSE +DRAMATISEE +DRAMATISONS +DRAP +DRAPASSENT +DRAPEES +DRAPERAS +DRAPEZ +DRAVASSE +DRAVENT +DRAVERIEZ +DRAVIDIEN +DRAYAGE +DRAYASSENT +DRAYER +DRAYERIONS +DRAYOIRS +DREIGES +DRESSAI +DRESSASSES +DRESSERA +DRESSERONS +DRESSINGS +DREYFUSARDS +DRIBBLAS +DRIBBLEES +DRIBBLEREZ +DRIBBLEZ +DRILLAI +DRILLASSIEZ +DRILLERAI +DRILLERONT +DRINK +DRIVANT +DRIVEE +DRIVERENT +DRIVEZ +DROGUAIT +DROGUATES +DROGUERAIT +DROGUES +DROIDES +DROITISAI +DROITISER +DROITISTE +DROLERIES +DROME +DRONTES +DROPASSE +DROPENT +DROPERIEZ +DROPPAGE +DROPPASSENT +DROPPER +DROPPERIONS +DROSERACEE +DROSSAIS +DROSSAT +DROSSERAIS +DROSSEZ +DRUE +DRUIDISME +DRUMMEUSES +DRUPES +DUALISAIT +DUALISATES +DUALISES +DUALITES +DUBITATION +DUBS +DUCATS +DUCS +DUDGEONNES +DUEL +DUETTOS +DUITAGES +DUITASSES +DUITERA +DUITERONS +DULCICOLE +DULCIFIASSE +DULCIFIEE +DULCIFIERENT +DULCIFIONS +DUMPER +DUNETTE +DUODENITE +DUPAI +DUPASSIEZ +DUPERAI +DUPERIONS +DUPIONS +DUPLEXANT +DUPLEXEE +DUPLEXERENT +DUPLEXONS +DUPLICATIFS +DUPLIQUAIS +DUPLIQUAT +DUPLIQUERAIS +DUPLIQUEZ +DUQUAMES +DUQUE +DUQUERAIT +DUQUIEZ +DURAI +DURALES +DURASSENT +DURAUX +DURCIRAIT +DURCISSAIS +DURCISSEZ +DURENT +DURERIEZ +DURIAN +DURS +DUTES +DUVETA +DUVETASSES +DUVETERA +DUVETERONS +DUVETS +DUVETTERIEZ +DYARCHIE +DYNAMIQUE +DYNAMISANTE +DYNAMISATES +DYNAMISES +DYNAMITAGE +DYNAMITER +DYNAMITERIES +DYNAMITEZ +DYNASTE +DYSACOUSIE +DYSBARISME +DYSCALCULIE +DYSCRASIES +DYSENTERIE +DYSLOGIES +DYSMNESIE +DYSPEPTIQUES +DYSPHONIES +DYSPLASIQUES +DYSPROSODIES +DYSTHYMIES +DYSTONIQUES +DZETA +EBAHIMES +EBAHIRIEZ +EBAHISSEMENT +EBARBA +EBARBASSE +EBARBENT +EBARBERIEZ +EBARBIEZ +EBATTAIS +EBATTIRENT +EBATTRA +EBATTRONS +EBAUBIR +EBAUBIRIONS +EBAUBISSES +EBAUCHAI +EBAUCHASSIEZ +EBAUCHERAI +EBAUCHERONT +EBAUCHONS +EBAUDIRAIT +EBAUDISSAIS +EBAUDIT +EBAVURANT +EBAVUREE +EBAVURERENT +EBAVURONS +EBENISTES +EBERLUASSENT +EBERLUER +EBERLUERIONS +EBISELA +EBISELASSES +EBISELES +EBISELLERAIT +EBLOUIE +EBLOUIRENT +EBLOUISSANT +EBLOUISSIEZ +EBORGNAI +EBORGNASSIEZ +EBORGNER +EBORGNERIONS +EBOUAGES +EBOUASSES +EBOUERA +EBOUERONS +EBOULANT +EBOULEE +EBOULERAIT +EBOULEUSES +EBOURGEONNAT +EBOURIFFAIS +EBOURIFFERA +EBOURRAIS +EBOURRAT +EBOURRERAIS +EBOURREZ +EBOUTAIENT +EBOUTASSIONS +EBOUTERAIENT +EBOUTES +EBRANCHAIS +EBRANCHAT +EBRANCHERAI +EBRANCHERONT +EBRANLAIENT +EBRANLERA +EBRANLERONS +EBRASAIS +EBRASAT +EBRASERAI +EBRASERONT +EBRECHAIENT +EBRECHERA +EBRECHERONS +EBRIETES +EBRIQUANT +EBRIQUEE +EBRIQUERENT +EBRIQUONS +EBROUAMES +EBROUE +EBROUERAIS +EBROUEZ +EBRUITANT +EBRUITEE +EBRUITERAIT +EBRUITIEZ +EBURNEENS +ECACHAS +ECACHEES +ECACHEREZ +ECAILLA +ECAILLASSE +ECAILLENT +ECAILLERES +ECAILLEUSE +ECALAI +ECALASSIEZ +ECALERAI +ECALERONT +ECANGAGES +ECANGUASSE +ECANGUENT +ECANGUERIEZ +ECANGUIEZ +ECARQUILLEE +ECART +ECARTASSENT +ECARTELAI +ECARTELER +ECARTEMENT +ECARTERENT +ECARTIEZ +ECCHYMOTIQUE +ECHAFAUD +ECHAFAUDAS +ECHAFAUDEES +ECHAFAUDEREZ +ECHAFAUDS +ECHALASSERA +ECHALOTEE +ECHANCRANT +ECHANCREE +ECHANCRERENT +ECHANCRONS +ECHANGEAIT +ECHANGEATES +ECHANGERAIT +ECHANGEURS +ECHANSON +ECHAPPA +ECHAPPASSE +ECHAPPEE +ECHAPPERAIT +ECHAPPIEZ +ECHARDONNAIS +ECHARNANT +ECHARNEE +ECHARNERAIT +ECHARNEUSES +ECHAROGNAI +ECHAROGNERAI +ECHARPAIT +ECHARPATES +ECHARPERAIT +ECHARPIEZ +ECHASSIER +ECHAUDA +ECHAUDASSE +ECHAUDEMENT +ECHAUDERENT +ECHAUDOIR +ECHAUFFANTE +ECHAUFFATES +ECHAUFFES +ECHAUGUETTES +ECHAUMASSENT +ECHAUMER +ECHAUMERIONS +ECHEANCES +ECHEES +ECHELONNERA +ECHENILLAGES +ECHENILLERA +ECHENILLOIR +ECHERAS +ECHEVEAUX +ECHEVELERENT +ECHEVELLERAI +ECHEVELLES +ECHEVINAT +ECHIFFA +ECHIFFASSES +ECHIFFERA +ECHIFFERONS +ECHINAI +ECHINASSIEZ +ECHINERAI +ECHINERONT +ECHINOCOQUE +ECHIQUETE +ECHOGRAPHIAI +ECHOIRAIT +ECHOPPA +ECHOPPASSES +ECHOPPERA +ECHOPPERONS +ECHOSONDEUR +ECHOUAI +ECHOUASSIEZ +ECHOUER +ECHOUERIONS +ECHUES +ECIMAI +ECIMASSIEZ +ECIMERAI +ECIMERONT +ECLABOUSSAIT +ECLABOUSSE +ECLABOUSSIEZ +ECLAFAIS +ECLAFAT +ECLAFERAIS +ECLAFEZ +ECLAIRAGISTE +ECLAIRANTS +ECLAIRCIE +ECLAIRCIRENT +ECLAIRCISSEZ +ECLAIREMENTS +ECLAIREREZ +ECLAIREZ +ECLANCHES +ECLATANTES +ECLATE +ECLATERAIS +ECLATEUR +ECLECTISMES +ECLIPSAS +ECLIPSEES +ECLIPSEREZ +ECLIPTIQUE +ECLISSANT +ECLISSEE +ECLISSERENT +ECLISSONS +ECLORAIS +ECLOSANT +ECLOSOIR +ECLUSAIT +ECLUSATES +ECLUSERAIT +ECLUSIER +ECOBILAN +ECOBUANT +ECOBUEE +ECOBUERENT +ECOBUONS +ECOEURANTE +ECOEURERA +ECOEURERONS +ECOGRAPHIES +ECOLAGES +ECOLOGIE +ECOLOGUES +ECONDUIRE +ECONDUISANT +ECONDUISISSE +ECONDUITES +ECONOMETRIE +ECONOMISAI +ECONOMISERAI +ECONOMISTE +ECOPANT +ECOPATES +ECOPERAIT +ECOPES +ECOPRODUITS +ECORCAMES +ECORCE +ECORCERAS +ECORCEUSE +ECORCHAMES +ECORCHE +ECORCHERAIS +ECORCHERONT +ECORCHURE +ECORNAI +ECORNASSIEZ +ECORNERAI +ECORNERONT +ECORNIFLANT +ECORNIFLEE +ECOSPHERE +ECOSSAIT +ECOSSATES +ECOSSERAIT +ECOSSEURS +ECOTA +ECOTASSE +ECOTEE +ECOTERENT +ECOTEUSES +ECOTOXICITES +ECOULA +ECOULASSES +ECOULENT +ECOULERIEZ +ECOUMENES +ECOURTAS +ECOURTEES +ECOURTEREZ +ECOURTICHAIS +ECOUTAIS +ECOUTASSES +ECOUTERA +ECOUTERONS +ECOUTILLES +ECOUVILLONS +ECRAPOUTI +ECRAPOUTIRAS +ECRASA +ECRASAS +ECRASEES +ECRASERAS +ECRASEUSE +ECREMAIENT +ECREMASSIONS +ECREMERAIENT +ECREMES +ECREMONS +ECRETAS +ECRETEES +ECRETERAS +ECRETEZ +ECRIAIT +ECRIATES +ECRIERAIT +ECRIIEZ +ECRIRAS +ECRITEAU +ECRIVAILLAI +ECRIVAINS +ECRIVASSANT +ECRIVASSENT +ECRIVASSIEZ +ECRIVIRENT +ECROU +ECROUASSENT +ECROUENT +ECROUERIEZ +ECROUIMES +ECROUIREZ +ECROUISSAIT +ECROUITES +ECROULASSENT +ECROULEMENTS +ECROULEREZ +ECROUONS +ECROUTANT +ECROUTEE +ECROUTERENT +ECROUTIEZ +ECSTASIES +ECTINITES +ECTOTHERMES +ECUELLE +ECUISSANT +ECUISSEE +ECUISSERENT +ECUISSONS +ECULASSENT +ECULER +ECULERIONS +ECUMAGES +ECUMAS +ECUMEES +ECUMEREZ +ECUMEUX +ECURAI +ECURASSIEZ +ECURERAI +ECURERONT +ECURIES +ECUSSONNES +ECUYERES +EDAPHIQUES +EDENTAIT +EDENTATES +EDENTERAIT +EDENTIEZ +EDICTAS +EDICTEES +EDICTEREZ +EDICTONS +EDIFIANTE +EDIFIATES +EDIFIEE +EDIFIERENT +EDIFIONS +EDIT +EDITASSENT +EDITER +EDITERIONS +EDITIONNAI +EDITIONNERAI +EDITONS +EDITS +EDREDONS +EDULCORAIS +EDULCORASSES +EDULCORENT +EDULCORERIEZ +EDUQUAI +EDUQUASSIEZ +EDUQUERAI +EDUQUERONT +EFAUFILAIT +EFAUFILATES +EFAUFILERAIT +EFAUFILIEZ +EFFACAI +EFFACASSIEZ +EFFACER +EFFACERIONS +EFFACURE +EFFANANT +EFFANEE +EFFANERENT +EFFANIEZ +EFFARAMES +EFFARASSIONS +EFFARERA +EFFARERONS +EFFAROUCHAIS +EFFAROUCHEZ +EFFECTIF +EFFECTUAI +EFFECTUERAI +EFFECTUERONT +EFFEMINAIT +EFFEMINATES +EFFEMINES +EFFERENTS +EFFEUILLAGE +EFFEUILLAS +EFFEUILLEES +EFFEUILLERAS +EFFEUILLEZ +EFFICIENCES +EFFILAI +EFFILASSIEZ +EFFILER +EFFILERIONS +EFFILIONS +EFFILOCHAS +EFFILOCHEES +EFFILOCHERAS +EFFILOCHEUSE +EFFILURES +EFFLANQUER +EFFLEURAGES +EFFLEURASSES +EFFLEURENT +EFFLEURERIEZ +EFFLEURIMES +EFFLEURIREZ +EFFLEURISSE +EFFLORAISON +EFFLUENT +EFFLUVANT +EFFLUVENT +EFFLUVERIEZ +EFFONDRAI +EFFONDRER +EFFORCAI +EFFORCASSIEZ +EFFORCERAI +EFFORCERONT +EFFRAIE +EFFRAIERIONS +EFFRANGEAMES +EFFRANGEE +EFFRANGERAIS +EFFRANGEZ +EFFRAYANTE +EFFRAYATES +EFFRAYERAIT +EFFRAYIEZ +EFFRITAIS +EFFRITAT +EFFRITERAI +EFFRITERONT +EFFRONTEES +EFFUSION +EGAIE +EGAIEREZ +EGAILLAIT +EGAILLATES +EGAILLERAIT +EGAILLIEZ +EGALAIT +EGALATES +EGALERAIS +EGALEZ +EGALISAS +EGALISATION +EGALISERAI +EGALISERONT +EGALITARISME +EGARAIS +EGARAT +EGARER +EGARERIONS +EGAYAI +EGAYASSE +EGAYEMENT +EGAYERENT +EGAYONS +EGERMAIS +EGERMAT +EGERMERAIS +EGERMEZ +EGLEFIN +EGLOMISAIT +EGLOMISATES +EGLOMISES +EGOCENTRISTE +EGOISTES +EGORGEASSE +EGORGEMENTS +EGORGERENT +EGORGEUSES +EGOSILLANT +EGOSILLEE +EGOSILLERENT +EGOSILLONS +EGOUTIER +EGOUTTAIS +EGOUTTAT +EGOUTTERAI +EGOUTTERONT +EGRAINA +EGRAINASSE +EGRAINEMENT +EGRAINERENT +EGRAINONS +EGRAPPAS +EGRAPPEES +EGRAPPEREZ +EGRAPPOIRS +EGRATIGNASSE +EGRATIGNENT +EGRATIGNIEZ +EGRENAGE +EGRENASSENT +EGRENEMENTS +EGRENEREZ +EGRENIONS +EGRISAGES +EGRISASSES +EGRISERA +EGRISERONS +EGROTANTS +EGRUGEANT +EGRUGEES +EGRUGERAIT +EGRUGIEZ +EGUEULASSE +EGUEULEMENT +EGUEULERENT +EGUEULONS +EHONTE +EIDETISMES +EJACULAIENT +EJACULERAIS +EJACULEZ +EJECTAIS +EJECTAT +EJECTERAIS +EJECTEUR +EJECTONS +EJOINTAIT +EJOINTATES +EJOINTERAIT +EJOINTIEZ +ELABORAMES +ELABORATION +ELABORERAIS +ELABOREZ +ELAGUAIS +ELAGUAT +ELAGUERAIS +ELAGUEUR +ELAMITE +ELANCAS +ELANCEES +ELANCERAS +ELANCIONS +ELARGIE +ELARGIRENT +ELARGISSANT +ELARGISSIONS +ELASTOMERE +ELBEUF +ELEATIQUE +ELECTIVE +ELECTRICIEN +ELECTRIFIAIT +ELECTRISAIS +ELECTRISENT +ELECTROCHOCS +ELECTRONIQUE +ELECTROPHORE +ELECTUAIRE +ELEGIAQUE +ELEGIRAIT +ELEGISSAIS +ELEGIT +ELEOMETRES +ELEPHANTIN +ELEVAI +ELEVASSIEZ +ELEVATRICE +ELEVERAIS +ELEVEUR +ELFES +ELIASSENT +ELIDAIT +ELIDATES +ELIDERAIT +ELIDIEZ +ELIERAIENT +ELIES +ELIMAIENT +ELIMASSIONS +ELIMERAIENT +ELIMES +ELIMINAS +ELIMINATION +ELIMINER +ELIMINERIONS +ELINDE +ELINGUANT +ELINGUEE +ELINGUERENT +ELINGUONS +ELIRE +ELISION +ELITISTE +ELLIPTIQUES +ELOGIEUX +ELOIGNASSENT +ELOIGNEMENTS +ELOIGNEREZ +ELOISE +ELONGEAMES +ELONGEE +ELONGERAS +ELONGIONS +ELOXEES +ELUAMES +ELUATES +ELUCIDASSENT +ELUCIDEES +ELUCIDEREZ +ELUCUBRA +ELUCUBRASSES +ELUCUBRENT +ELUCUBRERIEZ +ELUDAI +ELUDASSIEZ +ELUDERAI +ELUDERONT +ELUER +ELUERIONS +ELUS +ELUT +ELYME +ELZEVIRIENNE +EMACIANT +EMACIATIONS +EMACIERAIENT +EMACIES +EMAILLAIENT +EMAILLERONS +EMAILLONS +EMANANT +EMANATIONS +EMANCIPAS +EMANCIPATION +EMANCIPERAI +EMANCIPERONT +EMANER +EMANERIONS +EMARGEAI +EMARGEASSIEZ +EMARGER +EMARGERIONS +EMASCULAIS +EMASCULAT +EMASCULERAI +EMASCULERONT +EMBACLAIS +EMBACLAT +EMBACLERAIS +EMBACLEZ +EMBALLAIENT +EMBALLASSENT +EMBALLEMENTS +EMBALLEREZ +EMBALLEZ +EMBARCATION +EMBARDOUFLER +EMBARQUAIENT +EMBARQUERA +EMBARQUERONS +EMBARRAIS +EMBARRASSAT +EMBARRASSIEZ +EMBARRERA +EMBARRERONS +EMBASEMENT +EMBASTILLAS +EMBAT +EMBATAS +EMBATEES +EMBATEREZ +EMBATIRENT +EMBATRA +EMBATRONS +EMBATTENT +EMBATTISSES +EMBATTRAIT +EMBATTUES +EMBAUCHAIENT +EMBAUCHES +EMBAUCHONS +EMBAUMASSENT +EMBAUMEMENTS +EMBAUMEREZ +EMBAUMEZ +EMBECQUANT +EMBECQUEE +EMBECQUERENT +EMBECQUONS +EMBEGUINER +EMBELLIES +EMBELLIREZ +EMBELLISSE +EMBELLISSIEZ +EMBETAMES +EMBETASSIONS +EMBETERA +EMBETERONS +EMBEURRAIS +EMBEURRAT +EMBEURRERAIS +EMBEURREZ +EMBLAVAIENT +EMBLAVERA +EMBLAVERONS +EMBLEMATIQUE +EMBOBELINANT +EMBOBELINEES +EMBOBINAI +EMBOBINERAI +EMBOBINERONT +EMBOIRAIT +EMBOITA +EMBOITANT +EMBOITEE +EMBOITERAIT +EMBOITIEZ +EMBOLIE +EMBOLUS +EMBOSSAMES +EMBOSSE +EMBOSSERAS +EMBOSSIONS +EMBOUAIT +EMBOUATES +EMBOUCANER +EMBOUCHAIENT +EMBOUCHES +EMBOUEE +EMBOUERENT +EMBOUONS +EMBOUQUEREZ +EMBOURBA +EMBOURBASSES +EMBOURBERA +EMBOURBERONS +EMBOURRER +EMBOUT +EMBOUTASSENT +EMBOUTERAI +EMBOUTERONT +EMBOUTIRA +EMBOUTIRONS +EMBOUTISSENT +EMBRANCHA +EMBRANCHENT +EMBRAQUAI +EMBRAQUERAI +EMBRAQUERONT +EMBRASAIT +EMBRASATES +EMBRASES +EMBRASSAIS +EMBRASSASSES +EMBRASSENT +EMBRASSERIEZ +EMBRASSIEZ +EMBRAYAIS +EMBRAYAT +EMBRAYERAIS +EMBRAYEUR +EMBREVAIT +EMBREVATES +EMBREVES +EMBRIGADAMES +EMBRIGADE +EMBRIGADEZ +EMBRINGUANT +EMBRINGUEE +EMBRINGUONS +EMBROCHAMES +EMBROCHE +EMBROCHERAIS +EMBROCHEZ +EMBRONCHANT +EMBRONCHEE +EMBRONCHIEZ +EMBROUILLEZ +EMBRUMAMES +EMBRUME +EMBRUMERAS +EMBRUMIONS +EMBRYON +EMBRYOSCOPIE +EMBUAIS +EMBUAT +EMBUCHASSE +EMBUCHENT +EMBUCHERIEZ +EMBUEE +EMBUERENT +EMBUGNAIENT +EMBUGNES +EMBUS +EMBUSQUAS +EMBUSQUEES +EMBUSQUEREZ +EMBUSSE +EMBUVAIS +EMECHAIS +EMECHAT +EMECHERAIS +EMECHEZ +EMENDANT +EMENDATIONS +EMENDERAIT +EMENDIEZ +EMERGEAIT +EMERGEATES +EMERGENTS +EMERGEREZ +EMERILLON +EMERISAIS +EMERISAT +EMERISERAIS +EMERISEZ +EMERVEILLA +EMERVEILLER +EMETIQUE +EMETTANT +EMETTRA +EMETTRIEZ +EMEUTIERE +EMIAIT +EMIATES +EMIERAIT +EMIETTAI +EMIETTASSIEZ +EMIETTER +EMIETTERIONS +EMIEZ +EMIGRANTS +EMIGRATIONS +EMIGRERAIT +EMIGRETTES +EMILIENS +EMINCASSE +EMINCENT +EMINCERIEZ +EMINENCE +EMIRAT +EMIRIENS +EMISSIEZ +EMITES +EMMAGASINAS +EMMAILLA +EMMAILLASSES +EMMAILLERA +EMMAILLERONS +EMMAILLOTAIS +EMMAILLOTEZ +EMMANCHANT +EMMANCHEE +EMMANCHERAIT +EMMANCHIEZ +EMMELAIS +EMMELAT +EMMELERAI +EMMELERONT +EMMENAGERA +EMMENAGERONS +EMMENAIS +EMMENAT +EMMENERAIS +EMMENEZ +EMMERDAIENT +EMMERDASSENT +EMMERDEMENTS +EMMERDEREZ +EMMERDEZ +EMMETRANT +EMMETREE +EMMETRERENT +EMMETRONS +EMMIELLAMES +EMMIELLE +EMMIELLERAS +EMMIELLIONS +EMMITONNASSE +EMMITONNENT +EMMITOUFLAI +EMMITOUFLEZ +EMMURASSE +EMMUREMENT +EMMURERENT +EMMURONS +EMONCTIONS +EMONDAMES +EMONDE +EMONDERAS +EMONDEUSE +EMORFILAGES +EMORFILASSES +EMORFILERA +EMORFILERONS +EMOTIFS +EMOTIONNANT +EMOTIONNAT +EMOTIONNER +EMOTIVES +EMOTTAMES +EMOTTE +EMOTTERAIS +EMOTTEUR +EMOUCHAI +EMOUCHASSIEZ +EMOUCHERAI +EMOUCHERONT +EMOUCHETAMES +EMOUCHETE +EMOUCHETS +EMOUD +EMOUDRIONS +EMOULE +EMOULONS +EMOULUSSIEZ +EMOUSSAIS +EMOUSSAT +EMOUSSERAI +EMOUSSERONT +EMOUSTILLAIT +EMOUSTILLEZ +EMOUVANTS +EMOUVRAIT +EMPAFFES +EMPAILLAS +EMPAILLEES +EMPAILLERAS +EMPAILLEUSE +EMPALAIT +EMPALATES +EMPALERAIENT +EMPALES +EMPALMAIT +EMPALMATES +EMPALMERAIT +EMPALMIEZ +EMPANACHAMES +EMPANACHE +EMPANACHERAS +EMPANACHIONS +EMPANNAIT +EMPANNATES +EMPANNERAIT +EMPANNIEZ +EMPAQUETAIT +EMPAQUETATES +EMPARAIENT +EMPARASSIONS +EMPARERAIENT +EMPARES +EMPATAMES +EMPATE +EMPATERAIS +EMPATEZ +EMPATTAIENT +EMPATTERA +EMPATTERONS +EMPAUMAI +EMPAUMASSIEZ +EMPAUMERAI +EMPAUMERONT +EMPECHAIENT +EMPECHERA +EMPECHERONS +EMPECHONS +EMPEGUASSENT +EMPEGUER +EMPEGUERIONS +EMPENAGE +EMPENNANT +EMPENNEE +EMPENNELANT +EMPENNELEE +EMPENNELLERA +EMPENNELLES +EMPENNERENT +EMPENNONS +EMPERLAS +EMPERLEES +EMPERLEREZ +EMPESA +EMPESASSE +EMPESENT +EMPESERIEZ +EMPESTAI +EMPESTASSIEZ +EMPESTERAI +EMPESTERONT +EMPETRAIT +EMPETRATES +EMPETRES +EMPIEGEANT +EMPIEGEES +EMPIEGERENT +EMPIERRA +EMPIERRASSES +EMPIERRENT +EMPIERRERIEZ +EMPIETAI +EMPIETASSIEZ +EMPIETERAI +EMPIETERONT +EMPIFFRAIT +EMPIFFRATES +EMPIFFRERAIT +EMPIFFRIEZ +EMPILAI +EMPILASSIEZ +EMPILER +EMPILERIONS +EMPILIONS +EMPIRASSE +EMPIRENT +EMPIRERIEZ +EMPIRIQUES +EMPLAFONNAT +EMPLAFONNIEZ +EMPLIE +EMPLIRENT +EMPLISSAIS +EMPLIT +EMPLOIERAS +EMPLOYASSE +EMPLOYER +EMPLOYONS +EMPLUMASSENT +EMPLUMER +EMPLUMERIONS +EMPOCHAIENT +EMPOCHES +EMPOIGNAIS +EMPOIGNASSES +EMPOIGNERA +EMPOIGNERONS +EMPOISE +EMPOISONNEE +EMPOISSAIT +EMPOISSATES +EMPOISSERAIT +EMPOISSIEZ +EMPOISSONNES +EMPORTA +EMPORTASSES +EMPORTENT +EMPORTERIEZ +EMPOSIEUS +EMPOTANT +EMPOTEE +EMPOTERAIT +EMPOTIEZ +EMPOURPRAS +EMPOURPREES +EMPOURPREREZ +EMPOUSSIERA +EMPOUSSIERAT +EMPREIGNEZ +EMPREINDRE +EMPREINTS +EMPRESSEREZ +EMPRESURA +EMPRESURASSE +EMPRESURENT +EMPRISES +EMPRISONNENT +EMPRUNTAI +EMPRUNTERAI +EMPRUNTERONT +EMPRUNTS +EMPUANTIRAIT +EMULAIT +EMULATES +EMULERA +EMULERONS +EMULSIFIA +EMULSIFIE +EMULSIFIERAS +EMULSIFIIONS +EMULSIONNERA +EMURENT +EMYDES +ENAMOURANT +ENAMOUREE +ENAMOURERENT +ENAMOURONS +ENARCHIES +ENARTHROSES +ENCABANAS +ENCABANEES +ENCABANEREZ +ENCABLURE +ENCADRANTES +ENCADRE +ENCADRERAIS +ENCADREUR +ENCAGEAI +ENCAGEASSIEZ +ENCAGER +ENCAGERIONS +ENCAGOULAIS +ENCAGOULAT +ENCAGOULEZ +ENCAISSAIENT +ENCAISSEREZ +ENCAISSIONS +ENCANAILLAIS +ENCANAILLEZ +ENCANTAMES +ENCANTE +ENCANTERAS +ENCANTEUSE +ENCAPSULERA +ENCAQUASSENT +ENCAQUEMENTS +ENCAQUEREZ +ENCART +ENCARTAS +ENCARTEES +ENCARTEREZ +ENCARTIONS +ENCARTONNERA +ENCARTOUCHES +ENCASERNAS +ENCASERNEES +ENCASERNEREZ +ENCASTELA +ENCASTELERA +ENCASTRABLE +ENCASTREREZ +ENCAUSTIQUA +ENCAUSTIQUEZ +ENCAVAIT +ENCAVATES +ENCAVERAIENT +ENCAVES +ENCEIGNAIS +ENCEIGNIRENT +ENCEINDRA +ENCEINDRONS +ENCEINTANT +ENCEINTEE +ENCEINTERENT +ENCEINTONS +ENCELLULASSE +ENCELLULONS +ENCENSASSE +ENCENSEMENT +ENCENSERENT +ENCENSEUSES +ENCEPHALES +ENCEPHALOIDE +ENCERCLAS +ENCERCLEES +ENCERCLERAS +ENCERCLIONS +ENCHAINASSE +ENCHAINEMENT +ENCHAINERENT +ENCHAINONS +ENCHANTEZ +ENCHASSAIENT +ENCHASSERA +ENCHASSERONS +ENCHATELAI +ENCHATELERAI +ENCHATONNAIT +ENCHATONNE +ENCHATONNIEZ +ENCHAUSSAS +ENCHAUSSEES +ENCHAUSSEREZ +ENCHEMISA +ENCHEMISASSE +ENCHEMISENT +ENCHERES +ENCHERIRAIT +ENCHERISSAIS +ENCHEVAUCHA +ENCHEVAUCHAT +ENCHEVETRANT +ENCHEVETREES +ENCHEVETRONS +ENCHIFRENAS +ENCHILADA +ENCLAVANT +ENCLAVEE +ENCLAVERAIT +ENCLAVIEZ +ENCLENCHAS +ENCLENCHEES +ENCLENCHERAS +ENCLENCHEZ +ENCLIQUETEZ +ENCLORA +ENCLORONS +ENCLOSONS +ENCLOUAIT +ENCLOUATES +ENCLOUERAIT +ENCLOUIEZ +ENCLUMETTES +ENCOCHAS +ENCOCHEES +ENCOCHERAS +ENCOCHIONS +ENCODANT +ENCODEE +ENCODERENT +ENCODEUSES +ENCOFFRAMES +ENCOFFRE +ENCOFFRERAS +ENCOFFRIONS +ENCOLLAIT +ENCOLLATES +ENCOLLERAIT +ENCOLLEURS +ENCOMBRAI +ENCOMBRASSE +ENCOMBREMENT +ENCOMBRERENT +ENCOMBRONS +ENCORDAS +ENCORDEES +ENCORDEREZ +ENCORE +ENCORNASSENT +ENCORNER +ENCORNERIONS +ENCORNURE +ENCOUBLASSE +ENCOUBLENT +ENCOUBLERIEZ +ENCOURAGEA +ENCOURAGEAS +ENCOURAIENT +ENCOURIR +ENCOURRIONS +ENCOURUS +ENCRAGES +ENCRASSAIENT +ENCRASSERA +ENCRASSERONS +ENCREE +ENCRERENT +ENCREUSES +ENCRONS +ENCROUTAMES +ENCROUTE +ENCROUTERAIS +ENCROUTEZ +ENCRYPTAIT +ENCRYPTATES +ENCRYPTERAIT +ENCRYPTIEZ +ENCUVAMES +ENCUVE +ENCUVERAIS +ENCUVEZ +ENDEMIES +ENDENTAIS +ENDENTAT +ENDENTERAI +ENDENTERONT +ENDETTAIT +ENDETTATES +ENDETTES +ENDEUILLAMES +ENDEUILLE +ENDEUILLERAS +ENDEUILLIONS +ENDEVASSE +ENDEVERA +ENDEVERONS +ENDIABLAIS +ENDIABLAT +ENDIABLERAIS +ENDIABLEZ +ENDIGUA +ENDIGUASSES +ENDIGUENT +ENDIGUERIEZ +ENDIMANCHAI +ENDIMANCHEZ +ENDISQUANT +ENDISQUEE +ENDISQUERENT +ENDISQUONS +ENDOCRANIENS +ENDOCTRINENT +ENDODERMIQUE +ENDOGE +ENDOLORIR +ENDOMETRIOME +ENDOMMAGEAIS +ENDOMMAGES +ENDOPHASIE +ENDOREISME +ENDORMENT +ENDORMIEZ +ENDORMIRENT +ENDORMISSENT +ENDORT +ENDOSMOSE +ENDOSSAIT +ENDOSSATAIRE +ENDOSSERA +ENDOSSERONS +ENDOSSURES +ENDUIRAIS +ENDUISAGE +ENDUISIEZ +ENDUISIT +ENDURAI +ENDURANTS +ENDURCIE +ENDURCIRENT +ENDURCISSANT +ENDURCIT +ENDURERAIS +ENDUREZ +ENEMA +ENERGIE +ENERGIVORES +ENERVANTE +ENERVATES +ENERVERA +ENERVERONS +ENFAITAIS +ENFAITAT +ENFAITER +ENFAITERIONS +ENFANT +ENFANTASSENT +ENFANTEMENTS +ENFANTEREZ +ENFANTIN +ENFARGEAIENT +ENFARGES +ENFARINANT +ENFARINEE +ENFARINERENT +ENFARINONS +ENFERMASSE +ENFERMEMENT +ENFERMERENT +ENFERMONS +ENFERRASSENT +ENFERRER +ENFERRERIONS +ENFEUS +ENFICHAIT +ENFICHATES +ENFICHERAIT +ENFICHIEZ +ENFIELLAS +ENFIELLEES +ENFIELLEREZ +ENFIEVRA +ENFIEVRASSES +ENFIEVRENT +ENFIEVRERIEZ +ENFILADE +ENFILAS +ENFILEES +ENFILERAS +ENFILEUSE +ENFIROUAPAIS +ENFLAMMAIENT +ENFLAMMES +ENFLASSIEZ +ENFLER +ENFLERIONS +ENFLEURAIT +ENFLEURATES +ENFLEURERAIT +ENFLEURIEZ +ENFOIREE +ENFONCAS +ENFONCEES +ENFONCERAS +ENFONCEUSE +ENFORCIES +ENFORCIREZ +ENFORCISSE +ENFOUIE +ENFOUIRENT +ENFOUISSANT +ENFOUISSIONS +ENFOURCHANT +ENFOURCHEE +ENFOURCHIEZ +ENFOURNAIS +ENFOURNAT +ENFOURNERAI +ENFOURNERONT +ENFREIGNIONS +ENFREIGNONS +ENFUIENT +ENFUIRENT +ENFUISSIEZ +ENFUMAIT +ENFUMATES +ENFUMERAIT +ENFUMIEZ +ENFUTAILLA +ENFUTAILLES +ENFUTASSE +ENFUTENT +ENFUTERIEZ +ENFUYAIS +ENGAGE +ENGAGEANTS +ENGAGEES +ENGAGERAIT +ENGAGIEZ +ENGAINANTES +ENGAINE +ENGAINERAS +ENGAINIONS +ENGAMASSE +ENGAMENT +ENGAMERIEZ +ENGANES +ENGAZONNEREZ +ENGEANCE +ENGENDRANT +ENGENDREE +ENGENDRERAIT +ENGENDRIEZ +ENGERBAMES +ENGERBE +ENGERBERAS +ENGERBIONS +ENGLOBA +ENGLOBASSES +ENGLOBERA +ENGLOBERONS +ENGLOUTIMES +ENGLOUTIRIEZ +ENGLUAGE +ENGLUASSENT +ENGLUEMENTS +ENGLUEREZ +ENGOBA +ENGOBASSE +ENGOBENT +ENGOBERIEZ +ENGOMMAGE +ENGOMMASSENT +ENGOMMER +ENGOMMERIONS +ENGONCAIENT +ENGONCERA +ENGONCERONS +ENGORGEAIENT +ENGORGERA +ENGORGERONS +ENGOUAIT +ENGOUATES +ENGOUERAIENT +ENGOUES +ENGOUFFRASSE +ENGOUFFRONS +ENGOURDIR +ENGRAIS +ENGRAISSAS +ENGRAISSEES +ENGRAISSERAS +ENGRAISSEUSE +ENGRANGEAI +ENGRANGER +ENGRAVAIS +ENGRAVAT +ENGRAVERAIS +ENGRAVEZ +ENGRELURE +ENGRENANT +ENGRENEE +ENGRENERAIT +ENGRENEURS +ENGROSSA +ENGROSSASSES +ENGROSSERA +ENGROSSERONS +ENGRUMELAIS +ENGRUMELAT +ENGRUMELIONS +ENGUEULAIS +ENGUEULAT +ENGUEULERAI +ENGUEULERONT +ENGUIRLANDE +ENHARDI +ENHARDIRAS +ENHARDISSAIT +ENHARDITES +ENHARNACHEE +ENHERBA +ENHERBASSES +ENHERBENT +ENHERBERIEZ +ENIEMES +ENIVRAIT +ENIVRASSIEZ +ENIVRER +ENIVRERIONS +ENJAMBAIENT +ENJAMBERA +ENJAMBERONS +ENJAMBONS +ENJAVELERENT +ENJEU +ENJOIGNIEZ +ENJOIGNIT +ENJOINDREZ +ENJOLA +ENJOLASSES +ENJOLENT +ENJOLERIEZ +ENJOLIEZ +ENJOLIVASSE +ENJOLIVEMENT +ENJOLIVERENT +ENJOLIVEUSES +ENJOUEES +ENJUGUANT +ENJUGUEE +ENJUGUERENT +ENJUGUONS +ENJUIVASSENT +ENJUIVER +ENJUIVERIONS +ENJUPONNES +ENKIKINAIS +ENKIKINASSES +ENKIKINERA +ENKIKINERONS +ENKYSTAIS +ENKYSTAT +ENKYSTERAI +ENKYSTERONT +ENLACAIT +ENLACATES +ENLACERAIENT +ENLACES +ENLAIDIMES +ENLAIDIRIEZ +ENLEVA +ENLEVASSE +ENLEVEMENT +ENLEVERENT +ENLEVONS +ENLIAS +ENLIASSER +ENLIE +ENLIERAS +ENLIGNAI +ENLIGNASSIEZ +ENLIGNER +ENLIGNERIONS +ENLIONS +ENLISASSENT +ENLISEMENTS +ENLISEREZ +ENLOGE +ENLOGEASSENT +ENLOGENT +ENLOGEREZ +ENLUMINAI +ENLUMINERAI +ENLUMINERONT +ENLUMINURE +ENNEASYLLABE +ENNEIGEAS +ENNEIGEMENT +ENNEIGERAS +ENNEIGEZ +ENNOBLIMES +ENNOBLIRIEZ +ENNOIE +ENNOIEREZ +ENNOYAIENT +ENNOYASSIONS +ENNOYIEZ +ENNUAGEANT +ENNUAGEES +ENNUAGERAIT +ENNUAGIEZ +ENNUIERAS +ENNUYAIENT +ENNUYASSENT +ENNUYERENT +ENOL +ENONCANT +ENONCEE +ENONCERENT +ENONCIATION +ENORGUEILLIE +ENORME +ENOUAI +ENOUASSIEZ +ENOUERAI +ENOUERONT +ENQUEREZ +ENQUERRAS +ENQUETAIS +ENQUETAT +ENQUETERAIS +ENQUETEUR +ENQUIERE +ENQUILLAMES +ENQUILLE +ENQUILLERAS +ENQUILLIONS +ENQUIQUINE +ENQUISES +ENRACINAIENT +ENRACINERA +ENRACINERONS +ENRAGEAIENT +ENRAGEASSENT +ENRAGER +ENRAGERIONS +ENRAIENT +ENRAIERONS +ENRAYAMES +ENRAYE +ENRAYERAIS +ENRAYEZ +ENREGIMENTE +ENREGISTRAS +ENREGISTREZ +ENRENANT +ENRENEE +ENRENERAIT +ENRENIEZ +ENRESINAS +ENRESINEES +ENRESINERAS +ENRESINIONS +ENRHUMASSE +ENRHUMENT +ENRHUMERIEZ +ENRICHIE +ENRICHIRENT +ENRICHISSANT +ENRICHISSIEZ +ENROBAIS +ENROBASSES +ENROBENT +ENROBERIEZ +ENROBONS +ENROCHASSENT +ENROCHEMENTS +ENROCHEREZ +ENROLA +ENROLASSES +ENROLENT +ENROLERIEZ +ENROLONS +ENROUASSENT +ENROUEMENTS +ENROUEREZ +ENROUILLES +ENROULAIENT +ENROULERA +ENROULERONS +ENROULONS +ENRUBANNANT +ENRUBANNEE +ENRUBANNONS +ENSABLASSENT +ENSABLEMENTS +ENSABLEREZ +ENSACHA +ENSACHASSE +ENSACHENT +ENSACHERIEZ +ENSACHIEZ +ENSAISINAS +ENSAISINEES +ENSAISINERAS +ENSAISINIONS +ENSANGLANTEZ +ENSAUVAGEES +ENSAUVAGIEZ +ENSEIGNANT +ENSEIGNAT +ENSEIGNERAI +ENSEIGNERONT +ENSELLEMENTS +ENSEMBLISTE +ENSEMENCASSE +ENSEMENCONS +ENSERRASSENT +ENSERRER +ENSERRERIONS +ENSEVELI +ENSEVELIRAS +ENSEVELISSEZ +ENSILAGES +ENSILASSES +ENSILERA +ENSILERONS +ENSIMAGE +ENSIMASSENT +ENSIMER +ENSIMERIONS +ENSOLEILLAT +ENSOLEILLES +ENSORCELAI +ENSORCELASSE +ENSORCELER +ENSORCELLE +ENSOUFRAIT +ENSOUFRATES +ENSOUFRERAIT +ENSOUFRIEZ +ENSOUTANAMES +ENSOUTANE +ENSOUTANERAS +ENSOUTANIONS +ENSUIFANT +ENSUIFEE +ENSUIFERENT +ENSUIFONS +ENSUIVIES +ENSUQUA +ENSUQUASSES +ENSUQUERA +ENSUQUERONS +ENTABLAIENT +ENTABLERA +ENTABLERONS +ENTACHAI +ENTACHASSIEZ +ENTACHERAI +ENTACHERONT +ENTAILLAGES +ENTAILLASSES +ENTAILLERA +ENTAILLERONS +ENTAMAI +ENTAMASSIEZ +ENTAMERAI +ENTAMERONT +ENTARTAGE +ENTARTASSENT +ENTARTER +ENTARTERIONS +ENTARTIONS +ENTARTRANT +ENTARTREE +ENTARTRERAIT +ENTARTRIEZ +ENTASSANT +ENTASSEE +ENTASSERAIT +ENTASSIEZ +ENTELLE +ENTENDE +ENTENDIONS +ENTENDONS +ENTENDRIONS +ENTENEBRES +ENTERAI +ENTERINA +ENTERINASSES +ENTERINENT +ENTERINERIEZ +ENTERIQUE +ENTEROKINASE +ENTERRAGE +ENTERRASSENT +ENTERREMENTS +ENTERREREZ +ENTES +ENTETANTS +ENTETEE +ENTETERAIT +ENTETIEZ +ENTHOUSIASMA +ENTICHA +ENTICHASSES +ENTICHENT +ENTICHERIEZ +ENTIERE +ENTOILA +ENTOILASSE +ENTOILENT +ENTOILERIEZ +ENTOIRS +ENTOLAS +ENTOLEES +ENTOLEREZ +ENTOLEZ +ENTONNAGES +ENTONNASSE +ENTONNEMENT +ENTONNERENT +ENTONNOIR +ENTORTILLER +ENTOURAGES +ENTOURASSES +ENTOURERA +ENTOURERONS +ENTOURNURE +ENTRACCUSER +ENTRACCUSEZ +ENTRADMIRANT +ENTRADMIRERA +ENTRADMIRIEZ +ENTRAIDIEZ +ENTRAINAIENT +ENTRAINEREZ +ENTRAINEZ +ENTRANTE +ENTRAPERCOIS +ENTRAPERCUS +ENTRASSES +ENTRAVAMES +ENTRAVE +ENTRAVERAS +ENTRAVIONS +ENTREBAT +ENTREBATTREZ +ENTRECHATS +ENTRECHOQUEZ +ENTRECOUPAT +ENTRECOUPIEZ +ENTRECROISAS +ENTRECROISER +ENTREE +ENTREGENT +ENTREGORGEE +ENTREJEUX +ENTRELACEREZ +ENTRELACS +ENTRELARDERA +ENTREMELEREZ +ENTREMET +ENTREMIRENT +ENTREMITES +ENTRENUIRE +ENTRENUISENT +ENTRENUISONS +ENTREPOSAIT +ENTREPOSATES +ENTREPOSEURS +ENTREPRENAIS +ENTREPRENDS +ENTREPRENNE +ENTRERAIS +ENTRESOLEE +ENTRETAILLER +ENTRETENANT +ENTRETENU +ENTRETINMES +ENTRETOILE +ENTRETUAIENT +ENTRETUEE +ENTREVIRENT +ENTREVOIENT +ENTREVOUTAIT +ENTREVOUTE +ENTREVOUTONS +ENTREVUE +ENTROBLIGE +ENTROBLIGENT +ENTROPIQUE +ENTROUVRAIT +ENTROUVRIRA +ENTROUVRONS +ENTUBAS +ENTUBEES +ENTUBEREZ +ENTURBANNE +ENUCLEAIT +ENUCLEATES +ENUCLEES +ENUMERAIS +ENUMERAT +ENUMEREES +ENUMEREREZ +ENUQUA +ENUQUASSES +ENUQUERA +ENUQUERONS +ENURETIQUES +ENVAHIRAIT +ENVAHISSAIS +ENVAHISSES +ENVAHITES +ENVASASSENT +ENVASEMENTS +ENVASEREZ +ENVELOPPA +ENVELOPPAS +ENVELOPPEES +ENVELOPPERAS +ENVELOPPIONS +ENVENIMASSE +ENVENIMEE +ENVENIMERAIT +ENVENIMIEZ +ENVERGEANT +ENVERGEES +ENVERGERENT +ENVERGIEZ +ENVERGUASSE +ENVERGUENT +ENVERGUERIEZ +ENVERGURES +ENVERRIEZ +ENVIAIENT +ENVIASSIONS +ENVIDAMES +ENVIDE +ENVIDERAS +ENVIDIONS +ENVIERAIS +ENVIEUSE +ENVINES +ENVIRONNANTE +ENVIRONNATES +ENVIRONNERAI +ENVISAGERA +ENVISAGERONS +ENVOILA +ENVOILASSES +ENVOILERA +ENVOILERONS +ENVOLAI +ENVOLASSIEZ +ENVOLER +ENVOLERIONS +ENVOUTAI +ENVOUTASSE +ENVOUTEMENT +ENVOUTERENT +ENVOUTEUSES +ENVOYAMES +ENVOYE +ENVOYEZ +ENZYMOLOGIE +EOHIPPUS +EOLIQUES +EOSINOPHILES +EPAGNEULE +EPAILLAMES +EPAILLE +EPAILLERAS +EPAILLIONS +EPAISSIE +EPAISSIRENT +EPAISSISSANT +EPAMPRAI +EPAMPRASSIEZ +EPAMPRER +EPAMPRERIONS +EPANCHA +EPANCHASSES +EPANCHENT +EPANCHERIEZ +EPANCHONS +EPANDES +EPANDIS +EPANDRAI +EPANDRONT +EPANNAIT +EPANNATES +EPANNELAIT +EPANNELATES +EPANNELLE +EPANNERAIS +EPANNEZ +EPANOUIRAI +EPANOUIRONT +EPAR +EPARGNANTE +EPARGNATES +EPARGNERAIT +EPARGNIEZ +EPARPILLAS +EPARPILLEES +EPARPILLERAS +EPARPILLEZ +EPARTS +EPATANT +EPATAT +EPATERAI +EPATERONT +EPAUFRA +EPAUFRASSES +EPAUFRERA +EPAUFRERONS +EPAULAI +EPAULASSENT +EPAULEMENTS +EPAULEREZ +EPAULIERES +EPECLA +EPECLASSES +EPECLERA +EPECLERONS +EPEES +EPELAMES +EPELE +EPELLATIONS +EPELLERIEZ +EPENTHETIQUE +EPEPINASSE +EPEPINENT +EPEPINERIEZ +EPERDUE +EPERONNAIS +EPERONNAT +EPERONNERAIS +EPERONNEZ +EPERVINS +EPEURANTS +EPEUREE +EPEURERENT +EPEURONS +EPHELIDES +EPHESIENS +EPIA +EPIAIT +EPIATES +EPICAMES +EPICASSES +EPICENE +EPICERA +EPICERIEZ +EPICIERES +EPICRANE +EPICURIENNES +EPICYCLOIDE +EPIDEMIQUE +EPIDIDYME +EPIE +EPIERAS +EPIERRAI +EPIERRASSIEZ +EPIERRER +EPIERRERIONS +EPIERRIONS +EPIGAMIQUE +EPIGENESES +EPIGLOTTIQUE +EPIGRAMME +EPIGYNE +EPILANT +EPILATEURS +EPILEPSIES +EPILERIEZ +EPILIEZ +EPILOGUAIT +EPILOGUATES +EPILOGUERAIT +EPILOGUIEZ +EPINAIES +EPINASSES +EPINCAIS +EPINCAT +EPINCELAMES +EPINCELE +EPINCELERAS +EPINCELIONS +EPINCERAIENT +EPINCES +EPINCETAS +EPINCETEES +EPINCETIONS +EPINCETTEREZ +EPINCEZ +EPINERAI +EPINERONT +EPINEUX +EPINGLANT +EPINGLEE +EPINGLERENT +EPINGLETTES +EPINIER +EPINONS +EPIPHYSAIRES +EPIPLOONS +EPISCLERITE +EPISODIQUE +EPISSAIT +EPISSATES +EPISSERAIT +EPISSIEZ +EPISTASIES +EPISTOLIERES +EPITEXTE +EPITHELIOME +EPITOME +EPIVARDAIT +EPIVARDATES +EPIVARDERAIT +EPIVARDIEZ +EPLOIERAI +EPLOIES +EPLOYAMES +EPLOYE +EPLUCHA +EPLUCHASSE +EPLUCHENT +EPLUCHERIEZ +EPLUCHEUSES +EPOCHES +EPOINTAIT +EPOINTATES +EPOINTES +EPONGEAGES +EPONGEASSES +EPONGERA +EPONGERONS +EPONTILLAGES +EPONTILLERA +EPONYMIES +EPOUILLAIS +EPOUILLAT +EPOUILLERAIS +EPOUILLEZ +EPOULARDAIT +EPOULARDATES +EPOULARDIEZ +EPOUMONAS +EPOUMONEES +EPOUMONEREZ +EPOUSA +EPOUSASSENT +EPOUSER +EPOUSERIONS +EPOUSSETA +EPOUSSETASSE +EPOUSSETER +EPOUSTOUFLA +EPOUSTOUFLAS +EPOUSTOUFLES +EPOUTIAIT +EPOUTIATES +EPOUTIERAIT +EPOUTIIEZ +EPOUTIRAS +EPOUTISSAIT +EPOUTITES +EPOUVANTAMES +EPOUVANTE +EPOUVANTEZ +EPOXYS +EPREIGNIMES +EPREIGNITES +EPREINDRIEZ +EPRENAIS +EPRENDRE +EPRENNE +EPRISES +EPROUVAI +EPROUVASSE +EPROUVENT +EPROUVERIEZ +EPROUVONS +EPUCANT +EPUCEE +EPUCERENT +EPUCONS +EPUISANTE +EPUISATES +EPUISERAIENT +EPUISES +EPULIES +EPURAIENT +EPURASSIONS +EPURATIVES +EPURERA +EPURERONS +EQUANIMITE +EQUARRIRAIS +EQUARRISSAGE +EQUARRISSES +EQUARRITES +EQUATORIENNE +EQUERRAMES +EQUERRE +EQUERRERAS +EQUERRIONS +EQUEUTAIT +EQUEUTATES +EQUEUTERAIT +EQUEUTIEZ +EQUIDISTANTE +EQUILIBRAGE +EQUILIBRANTS +EQUILIBREURS +EQUILIBRONS +EQUIN +EQUINOXIAUX +EQUIPANT +EQUIPATES +EQUIPERA +EQUIPERONS +EQUIPOLE +EQUIPONS +EQUISETALE +EQUITANTE +EQUIVALAIENT +EQUIVALEZ +EQUIVALUS +EQUIVAUX +EQUIVOQUERAI +ERADICABLE +ERADIQUAIENT +ERADIQUES +ERAFLAMES +ERAFLE +ERAFLERAIS +ERAFLEZ +ERAILLAIT +ERAILLATES +ERAILLES +ERASMIENS +ERECTEUR +EREINTA +EREINTANTES +EREINTE +EREINTERAIS +EREINTEUR +EREMITIQUE +ERESIPELE +EREVANAISES +ERGOGRAPHE +ERGOMETRIQUE +ERGOSTEROL +ERGOTAMES +ERGOTAT +ERGOTERAIS +ERGOTERONT +ERGOTS +ERIGEAMES +ERIGEE +ERIGERAS +ERIGIEZ +ERISTIQUE +ERODA +ERODASSES +ERODERA +ERODERONS +EROGENES +EROTIQUES +EROTISANTS +EROTISATIONS +EROTISERAIT +EROTISIEZ +ERPETOLOGIES +ERRAMES +ERRASSES +ERRATUMS +ERRERAS +ERREZ +ERSATZ +ERUBESCENTS +ERUCTAMES +ERUCTATION +ERUCTERAIS +ERUCTEZ +ERUGINEUSE +ERYTHRASMAS +ESBAUDIES +ESBAUDIREZ +ESBAUDISSE +ESBIGNAI +ESBIGNASSIEZ +ESBIGNERAI +ESBIGNERONT +ESBROUFAIT +ESBROUFATES +ESBROUFERAIT +ESBROUFEURS +ESCABECHES +ESCADRONNAT +ESCADRONNONS +ESCAGASSASSE +ESCAGASSENT +ESCALADAI +ESCALADERAI +ESCALADERONT +ESCALIER +ESCALOPASSE +ESCALOPENT +ESCALOPERIEZ +ESCAMOTABLE +ESCAMOTAS +ESCAMOTEES +ESCAMOTEREZ +ESCAMOTEZ +ESCARBILLE +ESCARPIN +ESCARRIFIAIT +ESCHANT +ESCHATES +ESCHERA +ESCHERONS +ESCLAFFA +ESCLAFFASSES +ESCLAFFERA +ESCLAFFERONS +ESCLAVAGEA +ESCLAVAGERAI +ESCLAVES +ESCOFFIAI +ESCOFFIERAI +ESCOFFIERONT +ESCOMPTABLE +ESCOMPTER +ESCOMPTIONS +ESCORTANT +ESCORTEE +ESCORTERENT +ESCORTIEZ +ESCOURGEONS +ESCRIMASSENT +ESCRIMER +ESCRIMERIONS +ESCRIMIONS +ESCROQUANT +ESCROQUEE +ESCROQUERENT +ESCROQUIEZ +ESERINES +ESGOURDER +ESKIMO +ESOTERIQUE +ESPACANT +ESPACEE +ESPACERAIT +ESPACIEZ +ESPAGNOLE +ESPARCET +ESPERAI +ESPERANTISTE +ESPERAT +ESPERERAIS +ESPEREZ +ESPINGOLE +ESPIONNAIS +ESPIONNAT +ESPIONNERAIS +ESPIONNEZ +ESPOIRS +ESPOUTIS +ESPRINGALES +ESQUICHAMES +ESQUICHE +ESQUICHERAS +ESQUICHIONS +ESQUIMAUDE +ESQUIMAUTANT +ESQUIMAUTER +ESQUINTAIENT +ESQUINTER +ESQUISSA +ESQUISSASSES +ESQUISSERA +ESQUISSERONS +ESQUIVAIS +ESQUIVAT +ESQUIVERAIS +ESQUIVEZ +ESSAIERAIS +ESSAIMA +ESSAIMASSE +ESSAIMENT +ESSAIMERIEZ +ESSAIS +ESSANGEANT +ESSANGEES +ESSANGERENT +ESSANVAGE +ESSARTAMES +ESSARTE +ESSARTERAIS +ESSARTEZ +ESSAYAIS +ESSAYAT +ESSAYERAIS +ESSAYEUR +ESSE +ESSENISMES +ESSENTIELLES +ESSONNIENNE +ESSORAIT +ESSORATES +ESSORERAIT +ESSOREUSES +ESSORILLAS +ESSORILLEES +ESSORILLERAS +ESSORILLIONS +ESSOUCHAIS +ESSOUCHAT +ESSOUCHERAI +ESSOUCHERONT +ESSOUFFLAIT +ESSOUFFLATES +ESSOUFFLES +ESSUIERAIS +ESSUYAGE +ESSUYASSENT +ESSUYERENT +EST +ESTAFILADES +ESTAMPAIS +ESTAMPAT +ESTAMPERAIS +ESTAMPEUR +ESTAMPILLES +ESTANCIAS +ESTERIFIAIS +ESTERIFIAT +ESTERIFIERAI +ESTHESIE +ESTHETICIEN +ESTHETISAIS +ESTHETISENT +ESTHETISONS +ESTIMAS +ESTIMATIF +ESTIMEES +ESTIMEREZ +ESTIVA +ESTIVAMES +ESTIVASSIONS +ESTIVER +ESTIVERIONS +ESTOCADES +ESTOMAQUANT +ESTOMAQUEE +ESTOMAQUONS +ESTOMPAS +ESTOMPEES +ESTOMPERAS +ESTOMPIONS +ESTOQUAIENT +ESTOQUES +ESTOURBIMES +ESTOURBIRIEZ +ESTRADIOL +ESTRAPADA +ESTRAPADERA +ESTRAPASSAIS +ESTROGENE +ESTROPIAIENT +ESTROPIES +ESTUARIENS +ETABLAI +ETABLASSIEZ +ETABLERAI +ETABLERONT +ETABLIRA +ETABLIRONS +ETABLISSENT +ETAGEA +ETAGEASSES +ETAGEONS +ETAGERES +ETAGISTE +ETAIERAIS +ETAINS +ETALAGEAIT +ETALAGEATES +ETALAGERAIT +ETALAGIEZ +ETALAS +ETALEES +ETALERAS +ETALEZ +ETALINGUAIT +ETALINGUATES +ETALINGUIEZ +ETALONNAI +ETALONNER +ETALONNIONS +ETAMAIT +ETAMATES +ETAMERA +ETAMERONS +ETAMINES +ETAMPAMES +ETAMPE +ETAMPERAS +ETAMPEUR +ETAMURE +ETANCHASSE +ETANCHEIFIA +ETANCHEIFIAT +ETANCHES +ETANCONNAIT +ETANCONNATES +ETANCONNES +ETAPE +ETARQUAIT +ETARQUATES +ETARQUERAIT +ETARQUIEZ +ETATISA +ETATISASSES +ETATISENT +ETATISERIEZ +ETATISONS +ETAYAIENT +ETAYASSIONS +ETAYERA +ETAYERONS +ETEIGNAIT +ETEIGNIS +ETEIGNONS +ETEINDRIONS +ETEND +ETENDENT +ETENDISSE +ETENDRA +ETENDRONS +ETERNELLES +ETERNISASSE +ETERNISENT +ETERNISERIEZ +ETERNITES +ETERNUASSENT +ETERNUER +ETERNUERIONS +ETESIENS +ETETAS +ETETEES +ETETERAS +ETETIONS +ETHANOATE +ETHEREE +ETHERIFIAS +ETHERIFIE +ETHERIFIERAS +ETHERIFIIONS +ETHERISASSE +ETHERISEE +ETHERISERENT +ETHERISME +ETHICIEN +ETHIQUE +ETHMOIDITES +ETHNICISAMES +ETHNICISEZ +ETHNIQUES +ETHNOGRAPHE +ETHNOLOGUE +ETHOLOGIE +ETHUSES +ETHYLIQUES +ETIAGE +ETIGEAIT +ETIGEATES +ETIGERAIT +ETIGIEZ +ETINCELANT +ETINCELAT +ETIOLAMES +ETIOLE +ETIOLERAIS +ETIOLEZ +ETIOPATHES +ETIQUETAIS +ETIQUETAT +ETIQUETEUSE +ETIRA +ETIRANT +ETIREE +ETIRERAIT +ETIREURS +ETOCS +ETOFFASSENT +ETOFFER +ETOFFERIONS +ETOILAIENT +ETOILASSIONS +ETOILERA +ETOILERONS +ETOLIENNE +ETONNANT +ETONNAT +ETONNERAI +ETONNERONT +ETOUFFAGES +ETOUFFAS +ETOUFFEES +ETOUFFERAS +ETOUFFEUSE +ETOUPAIENT +ETOUPASSIONS +ETOUPERAIENT +ETOUPES +ETOUPILLAS +ETOUPILLEES +ETOUPILLEREZ +ETOUPIONS +ETOURDIRA +ETOURDIRONS +ETOURDISSE +ETOURDITES +ETRANGETE +ETRANGLASSE +ETRANGLEMENT +ETRANGLERENT +ETRANGLEUSES +ETRECI +ETRECIRAS +ETRECISSAIT +ETRECITES +ETREIGNIMES +ETREIGNITES +ETREINDRIEZ +ETRENNAI +ETRENNASSIEZ +ETRENNERAI +ETRENNERONT +ETRILLAMES +ETRILLE +ETRILLERAS +ETRILLIONS +ETRIPANT +ETRIPEE +ETRIPERENT +ETRIPONS +ETRIQUASSENT +ETRIQUER +ETRIQUERIONS +ETRIVAIENT +ETRIVASSIONS +ETRIVERAIENT +ETRIVES +ETROITES +ETRONCONNAIS +ETUDIAIS +ETUDIASSES +ETUDIERA +ETUDIERONS +ETUVAGE +ETUVASSENT +ETUVEMENTS +ETUVEREZ +ETUVEZ +ETYMON +EUCARIDE +EUCLIDIENNES +EUDEMONISTES +EUES +EUGENOLS +EUMES +EUPATRIDES +EUPHEMIQUE +EUPHEMISAS +EUPHEMISEES +EUPHEMISEREZ +EUPHEMISMES +EUPHORISAIS +EUPHORISENT +EUPHOTIQUES +EUPLOIDES +EUPROCTES +EURASIENS +EUROBANQUE +EURODEPUTEES +EUROIS +EUROPEANISAI +EUROPIUM +EURYHALINE +EURYTHMIES +EUSKARA +EUSKERIENNES +EUSTASIES +EUTES +EUTHANASIAS +EUTHANASIENT +EUTHERIEN +EUTROPHIQUE +EVACUAMES +EVACUASSIONS +EVACUEE +EVACUERENT +EVACUONS +EVADASSENT +EVADER +EVADERIONS +EVALUA +EVALUASSE +EVALUATIFS +EVALUENT +EVALUERIEZ +EVANESCENCES +EVANGELISA +EVANGELISEE +EVANGELISMES +EVANOUIR +EVANOUIRIONS +EVAPORABLE +EVAPORASSENT +EVAPORATOIRE +EVAPORERAIS +EVAPOREZ +EVASAIT +EVASATES +EVASERAIENT +EVASES +EVASONS +EVEILLAIENT +EVEILLES +EVEINAGE +EVENTAI +EVENTAIS +EVENTAT +EVENTERAI +EVENTERONT +EVENTRAIT +EVENTRATES +EVENTRES +EVENTUEL +EVERSIONS +EVERTUASSENT +EVERTUER +EVERTUERIONS +EVHEMERISTE +EVIDAIT +EVIDATES +EVIDENTE +EVIDERENT +EVIDOIR +EVINCAIS +EVINCAT +EVINCERAI +EVINCERONT +EVISCERAIT +EVISCERATES +EVISCERES +EVITAI +EVITASSIEZ +EVITER +EVITERIONS +EVOCATEUR +EVOLUA +EVOLUASSES +EVOLUERA +EVOLUERONS +EVOQUAI +EVOQUASSIEZ +EVOQUERAI +EVOQUERONT +EVULSION +EXACERBAIS +EXACERBAT +EXACERBERAI +EXACERBERONT +EXACTEUR +EXAGERAIS +EXAGERAT +EXAGEREES +EXAGERERENT +EXAGERONS +EXALTANTS +EXALTATIONS +EXALTERAIT +EXALTIEZ +EXAMINAIT +EXAMINATES +EXAMINERA +EXAMINERONS +EXASPERAI +EXASPERASSE +EXASPEREE +EXASPERERENT +EXASPERONS +EXAUCASSENT +EXAUCEMENTS +EXAUCEREZ +EXCAVA +EXCAVASSES +EXCAVATRICES +EXCAVERAIT +EXCAVIEZ +EXCEDANTE +EXCEDATES +EXCEDERAI +EXCEDERONT +EXCELLAIT +EXCELLATES +EXCELLER +EXCELLERIONS +EXCENTRAIENT +EXCENTRERA +EXCENTRERONS +EXCENTRIQUES +EXCEPTASSE +EXCEPTENT +EXCEPTERIEZ +EXCIPA +EXCIPASSES +EXCIPERAIENT +EXCIPES +EXCISAIS +EXCISAT +EXCISERAIS +EXCISEUR +EXCITABILITE +EXCITANTE +EXCITATES +EXCITATRICES +EXCITERAIT +EXCITIEZ +EXCLAMAIENT +EXCLAMEE +EXCLAMERENT +EXCLAMONS +EXCLUIEZ +EXCLURE +EXCLUSION +EXCLUSIVITES +EXCOMMUNIAT +EXCOMMUNIEZ +EXCORIANT +EXCORIATIONS +EXCORIERAIT +EXCORIIEZ +EXCRETASSE +EXCRETENT +EXCRETERIEZ +EXCRETIONS +EXCURSIONNAI +EXCUSAMES +EXCUSE +EXCUSERAS +EXCUSIONS +EXECRAIT +EXECRATES +EXECRERAIENT +EXECRES +EXECUTAIS +EXECUTASSES +EXECUTERA +EXECUTERONS +EXECUTIONS +EXEDRES +EXEMPLARITE +EXEMPLIFIAT +EXEMPLIFIEZ +EXEMPTAMES +EXEMPTE +EXEMPTERAS +EXEMPTION +EXERCAMES +EXERCASSIONS +EXERCERAIENT +EXERCES +EXERESES +EXFILTRANT +EXFILTRERAIT +EXFILTRIEZ +EXFOLIANTE +EXFOLIATES +EXFOLIES +EXHALAISONS +EXHALAT +EXHALERAI +EXHALERONT +EXHAUSSAIENT +EXHAUSSERA +EXHAUSSERONS +EXHAUSTIFS +EXHEREDAIENT +EXHEREDERA +EXHEREDERONS +EXHIBAIS +EXHIBAT +EXHIBERAIS +EXHIBEZ +EXHILARANTES +EXHORTASSE +EXHORTEE +EXHORTERENT +EXHORTONS +EXHUMASSENT +EXHUMEES +EXHUMEREZ +EXIGE +EXIGEANTS +EXIGEES +EXIGERAIT +EXIGIBILITE +EXIGUITES +EXILAS +EXILEES +EXILEREZ +EXILS +EXISTAIS +EXISTASSES +EXISTERAIT +EXISTIEZ +EXODES +EXOGENOSES +EXONDASSE +EXONDEE +EXONDERAIT +EXONDIEZ +EXONERAS +EXONERE +EXONERERAS +EXONERIONS +EXORBITAI +EXORBITASSE +EXORBITENT +EXORBITERIEZ +EXORCISAI +EXORCISER +EXORCISIONS +EXOREIQUE +EXOTISMES +EXPANSIBLES +EXPANSIVES +EXPASSAS +EXPASSEES +EXPASSEREZ +EXPAT +EXPATRIEES +EXPATRIEREZ +EXPATS +EXPECTORAI +EXPECTORASSE +EXPECTOREE +EXPECTORONS +EXPEDIASSENT +EXPEDIENTE +EXPEDIERENT +EXPEDIONS +EXPEDITIVES +EXPERIMENTAL +EXPERTISAIS +EXPERTISAT +EXPERTISEZ +EXPIAIS +EXPIAT +EXPIE +EXPIERAS +EXPIIONS +EXPIRANTES +EXPIRATEUR +EXPIREES +EXPIREREZ +EXPLANT +EXPLICATION +EXPLICITANT +EXPLICITEZ +EXPLIQUANT +EXPLIQUEE +EXPLIQUERENT +EXPLIQUONS +EXPLOITAIT +EXPLOITER +EXPLOITIONS +EXPLORAS +EXPLORATION +EXPLORER +EXPLORERIONS +EXPLOSAIENT +EXPLOSES +EXPLOSIFS +EXPO +EXPORTABLES +EXPORTASSES +EXPORTERAIT +EXPORTIEZ +EXPOSAMES +EXPOSASSIONS +EXPOSERA +EXPOSERONS +EXPOSONS +EXPRESSOS +EXPRIMAMES +EXPRIME +EXPRIMERAS +EXPRIMIONS +EXPROPRIANT +EXPROPRIAT +EXPROPRIEES +EXPROPRIEREZ +EXPULSA +EXPULSASSE +EXPULSENT +EXPULSERIEZ +EXPULSIONS +EXPURGEAI +EXPURGERAI +EXPURGERONT +EXQUISITES +EXSUDAIS +EXSUDAT +EXSUDERA +EXSUDERONS +EXTASIAI +EXTASIASSIEZ +EXTASIERAI +EXTASIERONT +EXTENSIBLES +EXTENSIONS +EXTENUAIT +EXTENUASSIEZ +EXTENUER +EXTENUERIONS +EXTERIORISAS +EXTERIORISER +EXTERMINEE +EXTERMINONS +EXTERNALISEZ +EXTINCTEURS +EXTINGUIBLES +EXTIRPAS +EXTIRPATION +EXTIRPERAI +EXTIRPERONT +EXTORQUAIT +EXTORQUATES +EXTORQUERAIT +EXTORQUEURS +EXTOURNAI +EXTOURNERAI +EXTOURNERONT +EXTRACTIVES +EXTRADAMES +EXTRADE +EXTRADERAS +EXTRADIONS +EXTRADURES +EXTRAFRAICHE +EXTRAIRAIS +EXTRAIT +EXTRALUCIDES +EXTRAPLATE +EXTRAPOLAMES +EXTRAPOLEZ +EXTRARENAUX +EXTRASPORTIF +EXTRAVAGUENT +EXTRAVASERA +EXTRAVERTI +EXTRAYIONS +EXTREMIS +EXTRUDA +EXTRUDASSES +EXTRUDERA +EXTRUDERONS +EXTRUSIFS +EXTUBAMES +EXTUBE +EXTUBERAS +EXTUBIONS +EXULCERAIENT +EXULCERERA +EXULCERERONS +EXULTAIS +EXULTAT +EXULTERAIS +EXULTEZ +EYALET +FABLES +FABRICANTS +FABRIQUAI +FABRIQUERAI +FABRIQUERONT +FABULAIT +FABULATES +FABULENT +FABULERIEZ +FABULIEZ +FACADIERES +FACETIEUSES +FACETTASSE +FACETTENT +FACETTERIEZ +FACHAI +FACHASSIEZ +FACHERAI +FACHERIONS +FACHIONS +FACILE +FACILITANTE +FACILITATES +FACILITENT +FACILITERIEZ +FACONDE +FACONNANT +FACONNEE +FACONNERAIT +FACONNEURS +FACONNONS +FACTICITE +FACTITIFS +FACTORIELS +FACTORISAS +FACTORISE +FACTORISERAS +FACTORISIONS +FACTUELS +FACTURAS +FACTURE +FACTURERAS +FACTUREZ +FACULE +FACULTES +FADAS +FADE +FADERAIT +FADETS +FADO +FAGACEES +FAGOTA +FAGOTASSE +FAGOTENT +FAGOTERIEZ +FAGOTIER +FAGOUE +FAIBLESSE +FAIBLIRAS +FAIBLISSAIT +FAIBLISSIONS +FAIENCERIES +FAILLA +FAILLASSES +FAILLERA +FAILLERONS +FAILLIES +FAILLIRAS +FAILLISSAIT +FAILLITES +FAINEANTAIT +FAINEANTATES +FAINEANTISE +FAISABILITES +FAISANDAI +FAISANDER +FAISANDERIES +FAISANDIONS +FAISEUR +FAITAGE +FAITOUT +FALACHA +FALCIFORME +FALLACIEUSE +FALOTE +FALSIFIAS +FALSIFIERAI +FALSIFIERONT +FALUNAGE +FALUNASSENT +FALUNER +FALUNERIONS +FALUNS +FAMENNOISE +FAMILIALES +FAMILIAUX +FAMINE +FANAISONS +FANASSIONS +FANATISAIT +FANATISATES +FANATISES +FANATISONS +FANEES +FANEREZ +FANEZ +FANFARONNAT +FANFARONNONS +FANTAISIE +FANTASMASSES +FANTASMENT +FANTASMERIEZ +FANTASQUES +FANTOMAL +FANUM +FAQUIN +FARADS +FARANDOLANT +FARANDOLENT +FARANDOLIEZ +FARCAI +FARCASSIEZ +FARCERAI +FARCERONT +FARCIEZ +FARCIRAIT +FARCISSAIS +FARCIT +FARDAIT +FARDATES +FARDERA +FARDERONS +FARDS +FARFELUS +FARFOUILLAS +FARFOUILLENT +FARINACEE +FARINANT +FARINEE +FARINERENT +FARINEZ +FARLOUCHE +FAROUCHEMENT +FARTAGE +FARTASSENT +FARTER +FARTERIONS +FASCEE +FASCICULES +FASCINAI +FASCINASSE +FASCINATIONS +FASCINES +FASCISAMES +FASCISERA +FASCISERONS +FASCISTES +FASEILLERAI +FASEILLERONT +FASEYAIENT +FASEYASSIONS +FASEYERAIT +FASEYIEZ +FASSIES +FASTIGIE +FAT +FATALITES +FATIGABLES +FATIGUAMES +FATIGUE +FATIGUERAS +FATIGUIONS +FATRASIES +FAUBER +FAUCARD +FAUCARDAS +FAUCARDEES +FAUCARDERAS +FAUCARDEUSE +FAUCHAI +FAUCHARDS +FAUCHEE +FAUCHERENT +FAUCHETTES +FAUCHONS +FAUCONNIER +FAUFILA +FAUFILASSE +FAUFILENT +FAUFILERIEZ +FAUFILURE +FAUNISTIQUE +FAUSSAIT +FAUSSATES +FAUSSERAIS +FAUSSET +FAUSTIENNES +FAUTAS +FAUTER +FAUTERIONS +FAUTIF +FAUVERIE +FAVELA +FAVORABLES +FAVORISANTE +FAVORISATES +FAVORISERAIT +FAVORISIEZ +FAX +FAXASSENT +FAXER +FAXERIONS +FAYOT +FAYOTAS +FAYOTEES +FAYOTEREZ +FAYOTS +FEBRICULES +FECALES +FECIAUX +FECONDAIT +FECONDASSIEZ +FECONDE +FECONDERAS +FECONDIONS +FECULAMES +FECULE +FECULERA +FECULERIEZ +FECULIERE +FEDE +FEDERALISAI +FEDERALISERA +FEDERAMES +FEDERATEUR +FEDERAUX +FEDERERAIT +FEDERIEZ +FEELINGS +FEIGNANT +FEIGNIEZ +FEIGNIT +FEINDRAIS +FEINT +FEINTASSENT +FEINTER +FEINTERIONS +FEINTIONS +FELAMES +FELDSPATH +FELEES +FELEREZ +FELIBRIGES +FELICITEES +FELICITEREZ +FELIDE +FELLAGA +FELLE +FELONNES +FEMELOT +FEMINISAIT +FEMINISER +FEMINISTE +FEMORALES +FENDAIS +FENDART +FENDEUSES +FENDILLAS +FENDILLEES +FENDILLERAS +FENDILLIONS +FENDISSIONS +FENDRAIT +FENDUE +FENESTRAMES +FENESTRATION +FENESTRERAIS +FENESTREZ +FENETRAIS +FENETRAT +FENETRERAIS +FENETREZ +FENNEC +FENTON +FEODALITE +FERALE +FEREZ +FERIES +FERLAGES +FERLASSES +FERLERA +FERLERONS +FERMAGE +FERMANTES +FERMAUX +FERMENTAIENT +FERMENTASSES +FERMENTERAIT +FERMERAI +FERMERONT +FERMIER +FERMOIR +FEROIENNES +FERRAIENT +FERRAILLAS +FERRAILLEES +FERRAILLERAS +FERRAILLEUSE +FERRASSENT +FERRATISMES +FERRERA +FERRERONS +FERREUSES +FERROCERIUM +FERRONNERIES +FERROUTAIT +FERROUTATES +FERROUTERAIT +FERROUTIER +FERTILEMENT +FERTILISANT +FERTILISAT +FERTILISEES +FERTILISEREZ +FERTILITE +FERVENTE +FESSAMES +FESSE +FESSERAS +FESSIERE +FESTIF +FESTINAS +FESTINEES +FESTINEREZ +FESTINS +FESTIVITES +FESTOIERAS +FESTONNAIENT +FESTONNES +FESTOYAIT +FESTOYATES +FESTOYEUSES +FETAMES +FETASSIEZ +FETERAI +FETERONT +FETICHEURS +FETICHISAMES +FETICHISEZ +FETIDITE +FETUQUE +FEUIL +FEUILLAIS +FEUILLARDS +FEUILLEE +FEUILLERENT +FEUILLETA +FEUILLETASSE +FEUILLETER +FEUILLU +FEULAI +FEULASSIEZ +FEULERAI +FEULERONT +FEUTRAGE +FEUTRANTS +FEUTREE +FEUTRERENT +FEUTRINES +FEVIER +FIABILISAIS +FIABILISAT +FIABILISEZ +FIACRE +FIANCAILLES +FIANCASSIONS +FIANCERAIENT +FIANCES +FIASQUES +FIBRE +FIBRILLES +FIBROMATEUSE +FIBULAS +FICELAIENT +FICELASSIONS +FICELIER +FICELLERAIT +FICELONS +FICHAMES +FICHASSIONS +FICHERAIENT +FICHES +FICHOIR +FICOIDES +FICTIVE +FIDEISMES +FIDELEMENT +FIDELISASSE +FIDELISEE +FIDELISERENT +FIDELISONS +FIDUCIANT +FIEFFAIENT +FIEFFASSIONS +FIEFFERAIENT +FIEFFES +FIELS +FIENTASSE +FIENTERA +FIENTERONS +FIERAIENT +FIERIONS +FIES +FIEVREUX +FIFTIES +FIGEAIT +FIGEATES +FIGERAIENT +FIGES +FIGNOLAIT +FIGNOLATES +FIGNOLERAIT +FIGNOLEURS +FIGUERIE +FIGURAI +FIGURASSE +FIGURATIONS +FIGURERA +FIGURERONS +FIGURISME +FILABLES +FILAMENTEUSE +FILANDRES +FILAO +FILAT +FILE +FILERAS +FILET +FILETAS +FILETEES +FILETEREZ +FILETS +FILIALEMENT +FILIALISASSE +FILIALISEE +FILIALISONS +FILICOPHYTE +FILIGRANAI +FILIGRANERAI +FILIPENDULE +FILLETTE +FILMAGE +FILMASSENT +FILMER +FILMERIONS +FILMIONS +FILMOLOGIES +FILOCHAIENT +FILOCHES +FILONIEN +FILOUTA +FILOUTASSE +FILOUTENT +FILOUTERIE +FILOUTONS +FILTRAI +FILTRASSE +FILTRE +FILTRERAS +FILTRIONS +FINALISA +FINALISASSES +FINALISENT +FINALISERIEZ +FINALISONS +FINANCABLES +FINANCASSES +FINANCENT +FINANCERIEZ +FINANCIARISA +FINANCIARISE +FINANCIONS +FINASSASSE +FINASSERA +FINASSERIEZ +FINASSIER +FINAUDERIES +FINESSES +FINIRAIENT +FINIS +FINISSANTES +FINISSIEZ +FINITES +FINLANDISA +FINLANDISER +FINNOISES +FIOTES +FIREWIRE +FISCALE +FISCALISAS +FISCALISE +FISCALISERAS +FISCALISIONS +FISSENT +FISSIONNES +FISSIPEDE +FISSURASSE +FISSUREE +FISSURERENT +FISSURONS +FISTULEUSES +FITTA +FITTASSES +FITTERA +FITTERONS +FIXABLE +FIXANTE +FIXATES +FIXATRICES +FIXERAIS +FIXETTE +FIXINGS +FIZZ +FLACCIDES +FLACON +FLAG +FLAGELLAMES +FLAGELLATES +FLAGELLENT +FLAGELLERIEZ +FLAGELLUMS +FLAGEOLANTS +FLAGEOLEMENT +FLAGEOLERENT +FLAGEOLIEZ +FLAGORNAS +FLAGORNEES +FLAGORNEREZ +FLAGORNEUSE +FLAGRANTES +FLAIRANT +FLAIREE +FLAIRERENT +FLAIREUSES +FLAMANT +FLAMBANT +FLAMBAS +FLAMBEAUX +FLAMBERAIS +FLAMBERONT +FLAMBOIEMENT +FLAMBOIERIEZ +FLAMBOYAIT +FLAMBOYIEZ +FLAMINATS +FLAMMECHE +FLAMMETTES +FLANANT +FLANCHA +FLANCHASSES +FLANCHES +FLANDRE +FLANE +FLANERAIS +FLANERONT +FLANKERS +FLANQUANTES +FLANQUE +FLANQUERAIS +FLANQUEZ +FLAQUES +FLASHANT +FLASHAT +FLASHERAIS +FLASHEUSE +FLAT +FLATTASSE +FLATTENT +FLATTERIE +FLATULENTES +FLAVESCENT +FLAVINE +FLECHAIENT +FLECHASSIONS +FLECHERAIENT +FLECHES +FLECHIR +FLECHIRIONS +FLECHITES +FLEGMONEUSE +FLEMMARD +FLEMMARDERAI +FLEMMES +FLETRI +FLETRIRAS +FLETRISSAIT +FLETRISSONS +FLEURAGE +FLEURAS +FLEURDELISE +FLEURE +FLEURERAS +FLEURETAI +FLEURETONS +FLEURETTEREZ +FLEURIE +FLEURIRAIENT +FLEURIS +FLEURITES +FLEXIBILISA +FLEXIBILISAT +FLEXIONNELLE +FLEXUEUX +FLIBUSTAIS +FLIBUSTAT +FLIBUSTERAIS +FLIBUSTEZ +FLICAILLES +FLINGUAIENT +FLINGUES +FLINTS +FLIPPANT +FLIPPAT +FLIPPERAIS +FLIPPES +FLIQUAIT +FLIQUATES +FLIQUERAIT +FLIQUESSES +FLIRTAIENT +FLIRTASSIONS +FLIRTERAIT +FLIRTEURS +FLOC +FLOCONNAIENT +FLOCONNES +FLOCULA +FLOCULASSENT +FLOCULER +FLOCULERIONS +FLOGNARDE +FLOQUAI +FLOQUASSIEZ +FLOQUERAI +FLOQUERONT +FLORALES +FLORENTINES +FLORICOLES +FLORIDIENNE +FLORISSAIS +FLOSCULEUSE +FLOTTAGE +FLOTTANTE +FLOTTASSES +FLOTTEMENT +FLOTTERENT +FLOTTIEZ +FLOUAIT +FLOUATES +FLOUERAIT +FLOUIEZ +FLOUTAGES +FLOUTASSES +FLOUTERA +FLOUTERONS +FLOUZES +FLUAS +FLUATES +FLUCTUANTS +FLUCTUATIONS +FLUCTUERENT +FLUCTUONS +FLUERAI +FLUERONT +FLUIDIFIAI +FLUIDIFIASSE +FLUIDIFIEE +FLUIDIFIONS +FLUIDISAS +FLUIDISE +FLUIDISERAS +FLUIDISIONS +FLUOCOMPACTS +FLUORES +FLUORURATION +FLUSTRE +FLUTASSE +FLUTEE +FLUTERENT +FLUTIEZ +FLUVIATILE +FLUX +FLUXAS +FLUXEES +FLUXEREZ +FLYSURFS +FOCALISAIT +FOCALISATES +FOCALISES +FOEHN +FOEHNASSENT +FOEHNER +FOEHNERIONS +FOENES +FOETOPATHIES +FOGGARAS +FOHNAS +FOHNEES +FOHNEREZ +FOHNS +FOIRADES +FOIRANT +FOIREE +FOIRERENT +FOIREZ +FOISONNAIENT +FOISONNER +FOLACHE +FOLATRAIT +FOLATRATES +FOLATRERENT +FOLATRIEZ +FOLIACES +FOLICHONNAIT +FOLICHONNE +FOLIE +FOLIOSCOPE +FOLIOTAMES +FOLIOTATION +FOLIOTERAIS +FOLIOTEUR +FOLIQUES +FOLKLORISA +FOLKLORISER +FOLKS +FOLLETTE +FOLLICULINES +FOMENTAS +FOMENTATION +FOMENTERAI +FOMENTERONT +FON +FONCAMES +FONCE +FONCERAIS +FONCEUR +FONCIEZ +FONCTIONNAS +FONDAMENTALE +FONDAS +FONDATION +FONDER +FONDERIES +FONDEZ +FONDISSIONS +FONDRAIENT +FONDRONS +FONGIBLES +FONGOIDES +FONT +FONTANGES +FOOTBALLEUR +FOOTINGS +FORAIN +FORAMINES +FORAT +FORCAIS +FORCAT +FORCENEES +FORCERAS +FORCEUR +FORCIONS +FORCIRENT +FORCISSANT +FORCLORE +FORDISTES +FORERAIT +FORESTAGES +FORETS +FORFAIRE +FORFAITISAIT +FORFAITISIEZ +FORFICULE +FORGEAIS +FORGEAT +FORGERAIS +FORGES +FORINTS +FORJETAS +FORJETEES +FORJETTENT +FORJETTERONS +FORLANCAS +FORLANCEES +FORLANCEREZ +FORLANE +FORLIGNASSE +FORLIGNERA +FORLIGNERONS +FORLONGES +FORMAIT +FORMALISAMES +FORMALISEZ +FORMAMES +FORMASSIONS +FORMATANT +FORMATEE +FORMATERENT +FORMATIEZ +FORMATS +FORMENES +FORMERET +FORMIATES +FORMICIDES +FORMOL +FORMOLAS +FORMOLEES +FORMOLEREZ +FORMOLS +FORMULAIENT +FORMULASSES +FORMULENT +FORMULERIEZ +FORMYLES +FORTERESSE +FORTIFIAMES +FORTIFIERA +FORTIFIERONS +FORTIORI +FORTRANS +FORTUNELLA +FOSSANE +FOSSILIFERES +FOSSILISEES +FOSSILISEREZ +FOSSOIE +FOSSOIERIONS +FOSSOYAGES +FOSSOYASSES +FOSSOYES +FOUACE +FOUAILLAIT +FOUAILLATES +FOUAILLERAIT +FOUAILLIEZ +FOUDROIE +FOUDROIEREZ +FOUDROYAIENT +FOUDROYERENT +FOUET +FOUETTARDE +FOUETTATES +FOUETTES +FOUFOUS +FOUGEANT +FOUGEES +FOUGERAIT +FOUGERONS +FOUGUEUSES +FOUILLAIS +FOUILLAT +FOUILLERAIS +FOUILLEUR +FOUINA +FOUINARDS +FOUINENT +FOUINERIEZ +FOUINIEZ +FOUIRENT +FOUISSAIS +FOUISSEUSE +FOULAGES +FOULANTES +FOULAT +FOULERAIS +FOULERONT +FOULIONS +FOULONNAIT +FOULONNATES +FOULONNERAIT +FOULONNIER +FOULURE +FOURBIMES +FOURBIRIEZ +FOURBISSANT +FOURBIT +FOURCHAIENT +FOURCHES +FOURCHIEZ +FOURGONNAI +FOURGONNERAI +FOURGUAI +FOURGUASSIEZ +FOURGUERAI +FOURGUERONT +FOURME +FOURMILLAI +FOURMILLASSE +FOURMILLENT +FOURNAISE +FOURNIERES +FOURNIRAI +FOURNIRONT +FOURNISSES +FOURNITES +FOURRAGEAMES +FOURRAGEE +FOURRAGERAS +FOURRAGES +FOURRAMES +FOURRE +FOURRERAIS +FOURREUR +FOURRIONS +FOURVOIERAI +FOURVOIES +FOURVOYERENT +FOUTAGES +FOUTEAUX +FOUTIMASSAI +FOUTRAIENT +FOUTRE +FOUTU +FOX +FOYER +FRACASSANT +FRACASSAT +FRACASSERAI +FRACASSERONT +FRACTALS +FRACTIONNANT +FRACTIONNEES +FRACTIONNES +FRACTURA +FRACTURASSE +FRACTUREE +FRACTURERENT +FRACTURONS +FRAGILISANT +FRAGILISAT +FRAGILISERAI +FRAGMENTAI +FRAGMENTEES +FRAGMENTEREZ +FRAGMENTS +FRAICHE +FRAICHIRA +FRAICHIRONS +FRAICHISSEZ +FRAIERAIENT +FRAIRIE +FRAISAMES +FRAISE +FRAISERAIS +FRAISETTE +FRAISIONS +FRAMEES +FRANCHIES +FRANCHIREZ +FRANCHISES +FRANCHISSEZ +FRANCIENNE +FRANCISAI +FRANCISASSE +FRANCISCAINE +FRANCISES +FRANCISTES +FRANCOPHONE +FRANGEANTE +FRANGEATES +FRANGERAIT +FRANGIEZ +FRANGLAIS +FRANSQUILLON +FRAPE +FRAPPAIT +FRAPPASSIEZ +FRAPPER +FRAPPERIONS +FRAPPIONS +FRASASSE +FRASENT +FRASERIEZ +FRASONS +FRATERNISAI +FRATERNISEZ +FRATRIES +FRAUDASSENT +FRAUDEES +FRAUDEREZ +FRAUDEZ +FRAYA +FRAYASSE +FRAYEMENT +FRAYERE +FRAYEURS +FREAKS +FREDONNAMES +FREDONNE +FREDONNERAIS +FREDONNEZ +FREESIAS +FREGATAI +FREGATASSIEZ +FREGATERAI +FREGATERONT +FREINAGES +FREINASSES +FREINERA +FREINERONS +FREINONS +FRELATAIT +FRELATATES +FRELATERAIT +FRELATIEZ +FRELUQUETS +FREMIRENT +FREMISSANT +FREMISSIEZ +FRENATRICES +FREON +FREQUENTIEL +FRERECHES +FRET +FRETASSENT +FRETER +FRETERIONS +FRETILLAIENT +FRETILLER +FRETIONS +FRETTAMES +FRETTE +FRETTERAS +FRETTIONS +FRIABILITES +FRIBOURGEOIS +FRICASSAI +FRICASSERAI +FRICASSERONT +FRICHE +FRICOTAIS +FRICOTAT +FRICOTERAIS +FRICOTEUR +FRICTION +FRICTIONS +FRIGIDITES +FRIGORIFIAT +FRIGORIFIIEZ +FRILEUSE +FRIMAIRES +FRIMASSAIT +FRIMASSATES +FRIMASSERAIT +FRIMASSIEZ +FRIMERA +FRIMERONS +FRIMONS +FRINGILLIDES +FRINGUASSENT +FRINGUER +FRINGUERIONS +FRIOULANES +FRIPASSE +FRIPENT +FRIPERIE +FRIPIERES +FRIPONNAMES +FRIPONNE +FRIPONNERAS +FRIPONNEZ +FRIQUEE +FRIRAS +FRISAGES +FRISAS +FRISE +FRISERAIENT +FRISES +FRISON +FRISOTTANT +FRISOTTAT +FRISOTTERAIS +FRISOTTEZ +FRISSONNA +FRISSONNAS +FRISSONNEREZ +FRISSONS +FRITAIT +FRITATES +FRITERAIT +FRITES +FRITOT +FRITTAMES +FRITTE +FRITTERAS +FRITTIONS +FRIVOLEMENT +FROIDURES +FROISSANTE +FROISSATES +FROISSES +FROLAIS +FROLAT +FROLERAI +FROLERONT +FROLIONS +FROMAGERIE +FROMENTAUX +FRONCA +FRONCASSES +FRONCENT +FRONCERIEZ +FRONDA +FRONDASSE +FRONDENT +FRONDERIEZ +FRONDIEZ +FRONTALEMENT +FRONTEAUX +FRONTISTES +FROTTAIT +FROTTASSIEZ +FROTTER +FROTTERIONS +FROTTIONS +FROUAMES +FROUE +FROUEREZ +FROUFROUTAI +FROUFROUTER +FROUILLAIENT +FROUILLERAIT +FROUILLIEZ +FROUSSES +FRUCTIFIAI +FRUCTIFIASSE +FRUCTIFIERAS +FRUCTIFIIONS +FRUGALE +FRUITAGES +FRUMENTAIRE +FRUSTRAIS +FRUSTRASSES +FRUSTREE +FRUSTRERENT +FRUSTRONS +FUCALES +FUDGE +FUGACE +FUGUA +FUGUASSES +FUGUERA +FUGUERONS +FUGUONS +FUIRA +FUIRONS +FUITAI +FUITASSIEZ +FUITERAIS +FUITEZ +FULGURAIENT +FULGURANTE +FULGURATES +FULGURERAIS +FULGUREZ +FULIGULES +FULMINAI +FULMINASSE +FULMINATOIRE +FULMINERAIS +FULMINEZ +FUMAGES +FUMANT +FUMASSENT +FUMER +FUMERIES +FUMERONNA +FUMERONNES +FUMETERRES +FUMIER +FUMIGEA +FUMIGEASSES +FUMIGEONS +FUMIGERIEZ +FUMISTERIE +FUMURE +FUNE +FUNESTEMENT +FUNKY +FURCULAS +FURETAMES +FURETE +FURETEREZ +FURETEZ +FURFURACES +FURIBARDS +FURIEUX +FURONCULOSE +FURTIVITES +FUSAIOLES +FUSAS +FUSE +FUSELAIENT +FUSELASSIONS +FUSELIEZ +FUSELLEREZ +FUSEOLOGIES +FUSERIEZ +FUSIBLE +FUSILLADES +FUSILLASSES +FUSILLERA +FUSILLERONS +FUSILLONS +FUSIONNAMES +FUSIONNE +FUSIONNER +FUSSE +FUSTIBALES +FUSTIGEANT +FUSTIGEES +FUSTIGERENT +FUSTINE +FUTALS +FUTON +FUTURE +FUYAIS +FUYEZ +GABARIAGE +GABARIASSENT +GABARIER +GABARIERIONS +GABARITS +GABELLE +GABIONNAGE +GABIONNER +GABLES +GACHAI +GACHASSIEZ +GACHERAI +GACHERONT +GACHIONS +GADES +GADGETISASSE +GADGETISENT +GADIDE +GADJE +GADOUILLES +GADROUILLEZ +GAFFAIT +GAFFATES +GAFFERAIT +GAFFEURS +GAGAKUS +GAGEAMES +GAGEE +GAGERAS +GAGEUR +GAGISTES +GAGNAIENT +GAGNASSENT +GAGNER +GAGNERIES +GAGNEZ +GAIACOLS +GAILLARDE +GAILLETERIES +GAINAGES +GAINAS +GAINEES +GAINEREZ +GAINIERE +GAIZE +GALACTOMETRE +GALANDAGES +GALANTINES +GALAXIE +GALBANUMS +GALBEE +GALBERENT +GALBONS +GALEJAIENT +GALEJASSIONS +GALEJERAIT +GALEJIEZ +GALENISTES +GALERAIT +GALERATES +GALERERENT +GALERIENS +GALERUQUES +GALETAMES +GALETE +GALETS +GALETTERIEZ +GALEUX +GALIBOTS +GALILEENNE +GALIPETTE +GALIPOTAS +GALIPOTEES +GALIPOTEREZ +GALIPOTS +GALLEUSES +GALLICOLE +GALLIQUE +GALLOMANIE +GALOCHES +GALONNASSE +GALONNENT +GALONNERIEZ +GALONNIEZ +GALOPAIS +GALOPASSES +GALOPERA +GALOPERONS +GALOPINE +GALUCHATS +GALVANISAIS +GALVANISAT +GALVANISERAI +GALVAUDA +GALVAUDASSE +GALVAUDENT +GALVAUDERIEZ +GALVAUDIONS +GAMBADAIS +GAMBADAT +GAMBADERAS +GAMBADEUSE +GAMBERGEAI +GAMBERGERAI +GAMBERGERONT +GAMBIENNES +GAMBILLASSE +GAMBILLERA +GAMBILLERONS +GAMBITS +GAMELLAMES +GAMELLE +GAMELLERAS +GAMELLIONS +GAMETOCYTES +GAMINAIS +GAMINAT +GAMINERAS +GAMINEZ +GAMMARE +GAMOPETALE +GANACHES +GANGETIQUE +GANGRENER +GANGRENONS +GANJA +GANSAMES +GANSE +GANSERAS +GANSEZ +GANTAMES +GANTE +GANTER +GANTERIES +GANTIERS +GAPENCAISE +GARAGISTE +GARANCAGE +GARANCASSENT +GARANCER +GARANCERIES +GARANCEZ +GARANTIE +GARANTIRENT +GARANTISSANT +GARANTS +GARBURES +GARCONNIER +GARDAMES +GARDE +GARDERAI +GARDERIONS +GARDIANE +GARDIENS +GAREES +GARERAS +GARGAMELLES +GARGARISAMES +GARGARISE +GARGARISERAS +GARGARISIONS +GARGOTANT +GARGOTENT +GARGOTERIEZ +GARGOTIEZ +GARGOUILLONS +GARIEZ +GARNIE +GARNIRAIT +GARNISONS +GARNISSEUR +GARNITURE +GARROCHA +GARROCHASSES +GARROCHERA +GARROCHERONS +GARROTTAGE +GARROTTER +GARUMS +GASCONNAIT +GASCONNATES +GASCONNERENT +GASCONNISME +GASPESIEN +GASPILLAIS +GASPILLAT +GASPILLERAIS +GASPILLEUR +GASTERALES +GASTROLATRE +GASTROTOMIES +GATAMES +GATE +GATERAIS +GATERONT +GATIFIAI +GATIFIASSIEZ +GATIFIERAIS +GATIFIEZ +GATIONNE +GATTAIT +GATTATES +GATTERAIT +GATTIEZ +GAUCHERIE +GAUCHIRAI +GAUCHIRONT +GAUCHISSAIT +GAUCHISSONS +GAUDIE +GAUDIRENT +GAUDISSANT +GAUDRIOLE +GAUFRANT +GAUFREE +GAUFRERENT +GAUFRETTES +GAUFROIR +GAULAIS +GAULAT +GAULERAI +GAULERONT +GAULLIENNES +GAULOISERIES +GAUPE +GAUSSAMES +GAUSSE +GAUSSERAS +GAUSSEUR +GAUSSIONS +GAVAIT +GAVATES +GAVERAIT +GAVEURS +GAVONS +GAYAL +GAZAIT +GAZASSE +GAZEIFIA +GAZEIFIASSES +GAZEIFIENT +GAZEIFIERIEZ +GAZEIFORMES +GAZERAS +GAZETIERES +GAZIERES +GAZOLE +GAZONNAGE +GAZONNANTS +GAZONNEE +GAZONNERAIT +GAZONNEUSES +GAZOUILLEREZ +GAZOUILLEZ +GEANTES +GEGENE +GEIGNANTS +GEIGNEZ +GEIGNISSIONS +GEINDRE +GEISHAS +GELASIEN +GELATES +GELERAIT +GELIEZ +GELIFIANTES +GELIFICATION +GELIFIERAIS +GELIFIEZ +GELINOTTES +GELIVITE +GELULE +GEMELLEES +GEMIES +GEMINASSE +GEMINEE +GEMINERENT +GEMINONS +GEMIRIEZ +GEMISSANTES +GEMISSONS +GEMMAIT +GEMMATES +GEMMERAI +GEMMERONT +GEMMIONS +GENAI +GENANTES +GENAUX +GENDARMAS +GENDARMEES +GENDARMEREZ +GENDARMETTE +GENE +GENEPI +GENERALATS +GENERALISAIT +GENERALISEES +GENERASSES +GENEREE +GENERERENT +GENEREUX +GENES +GENETICIEN +GENETISMES +GENEVOIS +GENIAL +GENICULES +GENIEVRES +GENITALITE +GENITRICES +GENOCIDAMES +GENOCIDE +GENOCIDERAS +GENOCIDEUSE +GENOMES +GENOTOXIQUE +GENOUILLEE +GENRE +GENTIANACEE +GENTILLET +GENTRIES +GENUFLEXION +GEOCROISEURS +GEODESIQUES +GEOLOCALISAI +GEOMETRIDES +GEOMETRISAIT +GEOMETRISIEZ +GEOPELIES +GEOPOLITIQUE +GEOSCIENCES +GEOSTRATEGIE +GERA +GERANIACEE +GERANT +GERAT +GERBANT +GERBAT +GERBERAIS +GERBEUR +GERBILLE +GERCAIENT +GERCASSIONS +GERCERA +GERCERONS +GEREE +GERERENT +GERFAUT +GERMA +GERMANDREE +GERMANISANT +GERMANISAT +GERMANISERAI +GERMANITE +GERMASSE +GERMEN +GERMERENT +GERMIEZ +GERMINATIONS +GERMONS +GERSOISES +GESSES +GESTANTS +GESTATIONS +GESTICULAMES +GESTICULES +GESTIQUES +GHASSOUL +GHETTOISAS +GHETTOISE +GHETTOISERAS +GHETTOISIONS +GIBBSITE +GIBELOTTE +GIBOIERA +GIBOIERONT +GIBOYANT +GIBOYEE +GIBOYIONS +GICLAS +GICLEES +GICLERAS +GICLEZ +GIFLA +GIFLASSES +GIFLERA +GIFLERONS +GIGABIT +GIGANTISMES +GIGOGNE +GIGOTAIENT +GIGOTASSIONS +GIGOTERA +GIGOTERONS +GIGOTTE +GIGUANT +GIGUEE +GIGUERENT +GIGUEUSES +GILETIERES +GINDRE +GINGLARD +GINS +GIRAFEAUX +GIRASOL +GIRAVIATION +GIRODYNE +GIRON +GIRONNAI +GIRONNASSIEZ +GIRONNERAI +GIRONNERONT +GISAIENT +GISEMENTS +GITAI +GITAS +GITEES +GITEREZ +GITOLOGIES +GIVRAIS +GIVRASSES +GIVRERA +GIVRERONS +GIVRURE +GLACAIENT +GLACASSENT +GLACER +GLACERIES +GLACEUX +GLACIALEMENT +GLACIER +GLACIOLOGIES +GLACURES +GLAIEUL +GLAIRASSE +GLAIRENT +GLAIRERIEZ +GLAIRIONS +GLAISANT +GLAISEE +GLAISERENT +GLAISEZ +GLAMOUREUSES +GLANAMES +GLAND +GLANDAS +GLANDEES +GLANDEREZ +GLANDEZ +GLANDOUILLEZ +GLANDULEUSES +GLANERAI +GLANERONT +GLANURE +GLAPIRAIS +GLAPISSAIENT +GLAPISSENT +GLARONNAIS +GLATIR +GLATIRIONS +GLATISSES +GLAUCOME +GLAVIOT +GLAVIOTER +GLEBES +GLENAIS +GLENAT +GLENERAIS +GLENEZ +GLEY +GLINGLIN +GLISSA +GLISSANCE +GLISSASSENT +GLISSEMENTS +GLISSEREZ +GLISSEZ +GLOBAL +GLOBALISANT +GLOBALISAT +GLOBALISEES +GLOBALISEREZ +GLOBALISMES +GLOBULES +GLOIRE +GLORIA +GLORIFIAIENT +GLORIFIERAIS +GLORIFIEZ +GLOSAIENT +GLOSASSIONS +GLOSERAIENT +GLOSES +GLOSSECTOMIE +GLOSSODYNIE +GLOTTE +GLOUGLOUTAIT +GLOUGLOUTE +GLOUSSAI +GLOUSSASSE +GLOUSSENT +GLOUSSERIEZ +GLOUTERONS +GLU +GLUANTS +GLUAUX +GLUCINIUMS +GLUCOSE +GLUEES +GLUEREZ +GLUIS +GLUTAMINE +GLUTINEUX +GLYCERINAI +GLYCERINERAI +GLYCEROLES +GLYCOGENIE +GLYCOLS +GLYPTODON +GNANGNAN +GNEISSEUSES +GNIOLE +GNOME +GNOSE +GNOSTICISME +GOALS +GOBAS +GOBEES +GOBELINS +GOBERAS +GOBERGEANT +GOBERGEES +GOBERGERENT +GOBERIEZ +GOBICHONNA +GOBICHONNES +GOBIONS +GODAILLAIS +GODAILLAT +GODAILLERAS +GODAILLIONS +GODASSIEZ +GODEMICHES +GODERAS +GODETIAS +GODILLA +GODILLASSES +GODILLES +GODILLOTS +GODRONNAIENT +GODRONNES +GOEMON +GOETHITE +GOGEAMES +GOGEE +GOGERAS +GOGIONS +GOGUENARDS +GOINFRAIENT +GOINFRERONS +GOITREUSE +GOLFA +GOLFASSES +GOLFERAIENT +GOLFES +GOLFONS +GOMARISMES +GOMENOLS +GOMINASSENT +GOMINER +GOMINERIONS +GOMMAGES +GOMMAS +GOMMEES +GOMMEREZ +GOMMEUX +GOMMOSES +GONADIQUE +GONAKIER +GONDOLA +GONDOLANTES +GONDOLE +GONDOLERAIS +GONDOLEZ +GONELLE +GONFANONS +GONFLAMES +GONFLASSIONS +GONFLERA +GONFLERONS +GONFLONS +GONIOMETRIE +GONOCYTES +GONZE +GORD +GORET +GORGEAMES +GORGEE +GORGERAIT +GORGERINS +GORGONAIRE +GORON +GOSPLANS +GOSSASSENT +GOSSER +GOSSERIONS +GOTHAS +GOUACHA +GOUACHASSES +GOUACHERA +GOUACHERONS +GOUAILLAIS +GOUAILLAT +GOUAILLERAIS +GOUAILLERONT +GOUALANTE +GOUDRONNAGE +GOUDRONNER +GOUDRONNEUX +GOUFFRE +GOUGEAS +GOUGENT +GOUGERENT +GOUGIONS +GOUJATS +GOUJONNASSE +GOUJONNENT +GOUJONNERIEZ +GOUJONNIEZ +GOULAFRE +GOULEES +GOULOT +GOUMIER +GOUPILLANT +GOUPILLEE +GOUPILLERENT +GOUPILLON +GOURAMI +GOURASSES +GOURBIVILLES +GOUREN +GOURERENT +GOURGANE +GOURMANDAIS +GOURMANDAT +GOURMANDEZ +GOURMANTCHES +GOURNABLAI +GOURNABLERAI +GOUROUNSI +GOUT +GOUTASSENT +GOUTER +GOUTERIONS +GOUTEZ +GOUTTAMES +GOUTTE +GOUTTERAS +GOUTTEUR +GOUTTONS +GOUVERNAIL +GOUVERNANTS +GOUVERNEE +GOUVERNERAI +GOUVERNERONT +GOUVERNIONS +GOYESQUES +GRACIABLE +GRACIASSENT +GRACIER +GRACIERIONS +GRACIEZ +GRADA +GRADASSES +GRADEE +GRADERENT +GRADIENTS +GRADUAIENT +GRADUANT +GRADUATEURS +GRADUELLES +GRADUERENT +GRADUONS +GRAFFITAIS +GRAFFITAT +GRAFFITERAIS +GRAFFITEUR +GRAFFS +GRAFIGNER +GRAILLAIENT +GRAILLERA +GRAILLERONS +GRAILLONNAIS +GRAILLONS +GRAINANT +GRAINEE +GRAINERENT +GRAINETIERE +GRAINIERES +GRAISSAIENT +GRAISSES +GRAM +GRAMMAGES +GRAND +GRANDET +GRANDIRAI +GRANDIRONT +GRANDIT +GRANITAIENT +GRANITES +GRANITIQUES +GRANNYS +GRANULANT +GRANULATIONS +GRANULERAIS +GRANULEUSE +GRANUM +GRAPHEUR +GRAPHISMES +GRAPHITAMES +GRAPHITE +GRAPHITERAS +GRAPHITEUX +GRAPHOLOGIES +GRAPHOMETRES +GRAPPILLAI +GRAPPILLERAI +GRAPPILLONS +GRASSES +GRASSEYAMES +GRASSEYERA +GRASSEYERONS +GRASSOUILLET +GRATICULERA +GRATIFIAIS +GRATIFIASSES +GRATIFIENT +GRATIFIERIEZ +GRATINA +GRATINASSES +GRATINERA +GRATINERONS +GRATIS +GRATOUILLAIT +GRATOUILLE +GRATOUILLONS +GRATTAS +GRATTEES +GRATTERAIS +GRATTES +GRATTOIRS +GRATTOUILLAS +GRATTOUILLER +GRATUIT +GRAVAIENT +GRAVASSIONS +GRAVELAGE +GRAVELASSENT +GRAVELERENT +GRAVELLE +GRAVENT +GRAVERIEZ +GRAVEURS +GRAVIDITES +GRAVILLON +GRAVILLONNAS +GRAVILLONNES +GRAVIMETRES +GRAVIRAIS +GRAVISPHERE +GRAVISSIEZ +GRAVITAIRES +GRAVITASSENT +GRAVITE +GRAVITEREZ +GRAVITONS +GRAZIOSO +GREANT +GREBES +GRECISAIT +GRECISATES +GRECISERAIT +GRECISIEZ +GRECQUAIS +GRECQUAT +GRECQUERAIS +GRECQUEZ +GREDINS +GREERA +GREERONS +GREFFAIENT +GREFFASSIONS +GREFFERAIENT +GREFFES +GREFFIEZ +GREGARISME +GREIEZ +GRELASSE +GRELENT +GRELERIEZ +GRELEZ +GRELOTTAI +GRELOTTASSE +GRELOTTENT +GRELOTTERIEZ +GRELUCHES +GRENACHE +GRENADANT +GRENADEE +GRENADERENT +GRENADIEN +GRENADILLES +GRENAIENT +GRENAILLAS +GRENAILLEES +GRENAILLEREZ +GRENAIS +GRENASSIEZ +GRENELAIENT +GRENELIEZ +GRENELLEREZ +GRENERAI +GRENERONT +GRENIEZ +GRENOUILLAT +GRENOUILLERE +GRENUS +GRESAMES +GRESE +GRESERAS +GRESEUSE +GRESILLAIENT +GRESILLES +GRESONS +GREVANT +GREVEE +GREVERENT +GREVILLEAS +GRIBOUILLAGE +GRIBOUILLERA +GRIBOUILLONS +GRIFFAGES +GRIFFASSES +GRIFFERA +GRIFFERONS +GRIFFOIR +GRIFFONNAMES +GRIFFONNE +GRIFFONNEUR +GRIFFTONS +GRIGNAI +GRIGNAS +GRIGNER +GRIGNERIONS +GRIGNOTAGE +GRIGNOTEREZ +GRIGNOTEZ +GRIGRIS +GRILLAGE +GRILLAGER +GRILLAIENT +GRILLASSES +GRILLERA +GRILLERONS +GRILLS +GRIMACANTE +GRIMACATES +GRIMACERAIT +GRIMACIER +GRIMAIENT +GRIMASSIONS +GRIMERA +GRIMERONS +GRIMPAI +GRIMPASSE +GRIMPENT +GRIMPERENT +GRIMPEUR +GRIMPONS +GRINCANTS +GRINCEMENT +GRINCERENT +GRINCHAIENT +GRINCHERAIT +GRINCHEUSES +GRINGALETS +GRIOTTE +GRIPPAI +GRIPPASSE +GRIPPEES +GRIPPERAS +GRIPPIONS +GRISAILLAIS +GRISAILLAT +GRISAILLEZ +GRISANTS +GRISATES +GRISERIE +GRISETTES +GRISOLLANT +GRISOLLEMENT +GRISOLLERENT +GRISOLLONS +GRISONNANTES +GRISONNE +GRISONNERAS +GRISONNIONS +GRISOUTEUSES +GRIVELAS +GRIVELEES +GRIVELEZ +GRIVELLERAS +GRIVES +GRIVOISERIES +GROGNARD +GROGNASSAMES +GROGNASSENT +GROGNE +GROGNERAIS +GROGNERONT +GROGNONNA +GROGNONNES +GROINS +GROMMELAIS +GROMMELAT +GROMMELIONS +GROMMELLERAS +GRONDAIENT +GRONDASSENT +GRONDEMENTS +GRONDEREZ +GRONDEUSE +GROOVE +GROSSERIE +GROSSIERE +GROSSIRAI +GROSSIRONT +GROSSIT +GROSSOIEREZ +GROSSOYAIT +GROSSOYATES +GROSSOYONS +GROUILLAIT +GROUILLER +GROUP +GROUPALES +GROUPATES +GROUPERAI +GROUPERONT +GROUPIONS +GROWLERS +GRUERIES +GRUGEAS +GRUGENT +GRUGERAS +GRUGIONS +GRUMELAI +GRUMELASSIEZ +GRUMELEUSE +GRUMELONS +GRUPPETTI +GRUTAMES +GRUTE +GRUTERAS +GRUTIERE +GRYPHEE +GUANACO +GUARANI +GUEA +GUEASSE +GUEDES +GUEERAIT +GUEGUERRE +GUELTE +GUENONS +GUERBA +GUEREZAS +GUERIMES +GUERIRIEZ +GUERISSAIS +GUERISSEZ +GUERRE +GUERROYA +GUERROYASSES +GUERROYES +GUESDISME +GUETRAI +GUETRASSIEZ +GUETRERAI +GUETRERONT +GUETTAIENT +GUETTASSIONS +GUETTERAIENT +GUETTES +GUEULAI +GUEULARDES +GUEULE +GUEULERAIS +GUEULETON +GUEUSAIENT +GUEUSASSES +GUEUSERA +GUEUSERIEZ +GUEUZE +GUIBOLE +GUICHETIER +GUIDAIS +GUIDASSIEZ +GUIDER +GUIDERIONS +GUIDONS +GUIGNAMES +GUIGNASSIEZ +GUIGNERAI +GUIGNERONT +GUIGNOLADE +GUILDES +GUILLEMETEE +GUILLEMETTE +GUILLOCHAGES +GUILLOCHERA +GUILLOCHIS +GUILLOTINAIT +GUILLOTINE +GUIMBARDES +GUIMPASSENT +GUIMPER +GUIMPERIONS +GUINCHAIENT +GUINCHERAIT +GUINCHIEZ +GUINDAILLAT +GUINDAS +GUINDEAUX +GUINDERAS +GUINDEZ +GUINGOIS +GUIPAMES +GUIPE +GUIPERAS +GUIPIONS +GUISARME +GUITOUNES +GUMMIFERES +GUNITAS +GUNITEES +GUNITEREZ +GUNITIONS +GURU +GUSTATION +GUTTIFERE +GUYANIENNE +GYMKHANAS +GYMNASIENNE +GYMNOCARPES +GYNANDRIE +GYPSERIES +GYPSOPHILES +GYROLASERS +GYROPILOTES +GYROTRAIN +HABILETE +HABILITASSE +HABILITEE +HABILITERENT +HABILITONS +HABILLAMES +HABILLE +HABILLERAIS +HABILLEUR +HABITABILITE +HABITAMES +HABITASSIONS +HABITER +HABITERIONS +HABITUAI +HABITUASSIEZ +HABITUEES +HABITUES +HABLAIT +HABLATES +HABLERENT +HABLEURS +HACHA +HACHASSE +HACHEMENT +HACHERAIT +HACHES +HACHIS +HACHURAIT +HACHURATES +HACHURERAIT +HACHURIEZ +HACKEUSES +HADDOCKS +HADJI +HAFSIDES +HAGUENOVIENS +HAIE +HAINTENY +HAIRAIS +HAIRONT +HAISSES +HAITIENS +HALAI +HALANT +HALBIS +HALEE +HALENANT +HALENEE +HALENERENT +HALENONS +HALEREZ +HALETAIT +HALETASSIEZ +HALETERAI +HALETERONT +HALEUSE +HALIGONIEN +HALITE +HALLALIS +HALLOWEENS +HALLUCINAIT +HALLUCINEES +HALLUCINEREZ +HALOGENAIT +HALOGENATES +HALOGENES +HALOIRS +HALOPHYTES +HAMADRYAS +HAMECONNES +HAMMAM +HANAFISME +HANBALITE +HANCHASSE +HANCHEMENT +HANCHERENT +HANCHONS +HANDICAPAI +HANDICAPASSE +HANDICAPENT +HANDICAPONS +HANNETONNA +HANNETONNER +HANOIENNES +HANSARTS +HANTAIS +HANTAT +HANTERAIENT +HANTES +HAPALIDES +HAPLONTE +HAPPANT +HAPPEE +HAPPERAIENT +HAPPES +HAPTONOMIES +HARAKIRIS +HARANGUAS +HARANGUEES +HARANGUEREZ +HARANGUEZ +HARASSAIENT +HARASSASSENT +HARASSEMENTS +HARASSEREZ +HARCELA +HARCELAS +HARCELEES +HARCELERAS +HARCELEUSE +HARCELLERAIS +HARD +HARDASSENT +HARDEES +HARDEREZ +HARDEZ +HARDONS +HARENGERES +HARFANGS +HARIDELLES +HARKIS +HARMONISA +HARMONISENT +HARMONISTES +HARNACHAS +HARNACHEES +HARNACHERAS +HARNACHEZ +HARPAI +HARPAS +HARPEES +HARPEREZ +HARPIONS +HARPONNAIENT +HARPONNERA +HARPONNERONS +HARRAGA +HARUSPICES +HASARDASSE +HASARDENT +HASARDERIEZ +HASARDIEZ +HASCHISCHIN +HASSELTOISES +HAST +HATAI +HATASSIEZ +HATELETTES +HATERAS +HATEZ +HATIVEMENT +HAUBANAGES +HAUBANASSES +HAUBANERA +HAUBANERONS +HAUBERT +HAUSSASSE +HAUSSEMENT +HAUSSERENT +HAUSSIERES +HAUTAIN +HAUTERIVIEN +HAUTINS +HAVAGES +HAVANES +HAVE +HAVERAI +HAVERONT +HAVIEZ +HAVIRENT +HAVISSANT +HAVONS +HAWAIEN +HAZAN +HEAUMIERS +HEBELOMES +HEBERGEAIENT +HEBERGERA +HEBERGERONS +HEBERTISME +HEBETANT +HEBETEE +HEBETERAIT +HEBETIEZ +HEBRAISAI +HEBRAISASSE +HEBRAISENT +HEBRAISERIEZ +HEBRAISONS +HECTIQUES +HEDERACEES +HEDYCHIUMS +HEGEMONIQUES +HEIN +HELASSENT +HELEPOLE +HELEREZ +HELIANTHEMUM +HELICASES +HELICOIDE +HELIGARE +HELIODORES +HELIONS +HELIPORTAMES +HELIPORTE +HELIPORTERAS +HELIPORTIONS +HELITREUILLA +HELLADIQUES +HELLENISAIS +HELLENISENT +HELLENISONS +HELMINTHIQUE +HELODERME +HELVELLES +HELVETISMES +HEMATHIDROSE +HEMATITE +HEMATOLOGIE +HEMATOPHAGE +HEMATURIES +HEMIALGIES +HEMICYCLES +HEMINEE +HEMITROPES +HEMOCULTURES +HEMOGRAMME +HEMOPATHIE +HEMOPTYSIQUE +HEMOSTATIQUE +HENNINS +HENNIRIEZ +HENNISSANTES +HENNISSONS +HEPARINE +HEPATITE +HEPATOLOGUE +HEPTAEDRES +HEPTAMETRES +HERAULTAISES +HERBAGEAI +HERBAGER +HERBAGEREZ +HERBAGEZ +HERBASSE +HERBENT +HERBERIE +HERBEUSES +HERBIONS +HERBORISANT +HERBORISERA +HERBORISTES +HERCHAIS +HERCHAT +HERCHERAS +HERCHEUSE +HERCULEENS +HEREDITE +HERESIARQUES +HERISSAIS +HERISSAT +HERISSERAI +HERISSERONT +HERISSONNAIT +HERISSONNE +HERISSONNONS +HERITAIENT +HERITASSIONS +HERITERAIENT +HERITES +HERMANDADS +HERMETIQUE +HERMINETTES +HERNIEE +HEROIQUEMENT +HEROISASSE +HEROISEE +HEROISERENT +HEROISME +HERONS +HERSAIT +HERSATES +HERSCHAS +HERSCHER +HERSCHERIONS +HERSCHIONS +HERSERAIS +HERSEUR +HERTZIENNE +HESITAI +HESITASSE +HESITENT +HESITERIEZ +HESSOISE +HETERIE +HETEROCLITE +HETERODOXE +HETEROGAMIES +HETEROTHERME +HETTANGIEN +HEURE +HEURTA +HEURTASSES +HEURTERA +HEURTERONS +HEVEA +HEXADECIMAL +HEXAGONAL +HEXAMIDINE +HEXASTYLE +HIAIS +HIAT +HIBERNAIS +HIBERNAS +HIBERNAUX +HIBERNERENT +HIBERNIEZ +HIDALGOS +HIEMATIONS +HIERAIT +HIERAS +HIERODULE +HIERONS +HIFI +HIIONS +HILARES +HILOIRES +HIMALAYISMES +HINDIS +HINDOUSTANIE +HIPPARIONS +HIPPOLOGIES +HIPPOPHAGES +HIRCINE +HIRSUTISMES +HISPANISAI +HISPANISASSE +HISPANISEE +HISPANISME +HISPIDES +HISSAS +HISSEES +HISSEREZ +HISTAMINE +HISTOCHIMIE +HISTOLOGIES +HISTORIAI +HISTORIE +HISTORIERAI +HISTORIERONT +HISTORISME +HITTISTE +HIVERNAIS +HIVERNAS +HIVERNEE +HIVERNERAIT +HIVERNIEZ +HOBBYS +HOCHAIS +HOCHAT +HOCHEQUEUE +HOCHEREZ +HOCHIONS +HODOGRAPHE +HOLA +HOLLANDAISE +HOLOCAUSTE +HOLOGRAPHES +HOLOMORPHES +HOLOSTEEN +HOLSTEINS +HOMARDS +HOMEOSTAT +HOMERIQUE +HOMINIDES +HOMINISES +HOMO +HOMOGAMES +HOMOGENEISA +HOMOGENEISAT +HOMOGENEISEZ +HOMOGRAPHIES +HOMOLOGABLES +HOMOLOGUAIS +HOMOLOGUAT +HOMOLOGUEZ +HOMONCULE +HOMOPHOBIES +HOMOTHERME +HOMOZYGOTIE +HONGKONGAIS +HONGRAS +HONGREES +HONGREREZ +HONGRIONS +HONGROIERIE +HONGRONS +HONGROYAS +HONGROYEES +HONING +HONNIE +HONNIRENT +HONNISSANT +HONORA +HONORAIS +HONORASSIEZ +HONORERAI +HONORERONT +HONTEUSE +HOPAKS +HOQUETA +HOQUETASSES +HOQUETIONS +HOQUETTERAIT +HORAIRES +HORECA +HORLOGES +HORMONAMES +HORMONAUX +HORMONERAIT +HORMONIEZ +HORODATAIENT +HORODATES +HOROGRAPHIES +HORREURS +HORRIFIANT +HORRIFIAT +HORRIFIERAIS +HORRIFIEZ +HORRIPILAIT +HORRIPILEES +HORRIPILEREZ +HORS +HORTENSIAS +HOSPITALIERE +HOSPITALISAS +HOSPITALISER +HOSPODAR +HOSTIES +HOTDOG +HOTELS +HOTTAIS +HOTTAT +HOTTER +HOTTERET +HOTTEUSE +HOUACHE +HOUAMES +HOUAT +HOUBLONNAMES +HOUBLONNE +HOUBLONNERAS +HOUBLONNIERE +HOUEE +HOUERENT +HOUILLER +HOUKAS +HOULIGANISME +HOUPPA +HOUPPASSES +HOUPPENT +HOUPPERIEZ +HOUPPIEZ +HOURDAIENT +HOURDASSIONS +HOURDERAIENT +HOURDES +HOURQUES +HOUSEAU +HOUSPILLAS +HOUSPILLEES +HOUSPILLEREZ +HOUSPILLEZ +HOUSSAIES +HOUSSASSIONS +HOUSSERAIENT +HOUSSES +HOUSSINAMES +HOUSSINE +HOUSSINERAS +HOUSSINIONS +HOVERCRAFTS +HUAIS +HUASSENT +HUBLOTS +HUCHAS +HUCHEES +HUCHEREZ +HUCHIERS +HUERAI +HUERONT +HUGUENOTE +HUILAIENT +HUILASSIONS +HUILERAIENT +HUILERONS +HUILIERS +HUISSIERES +HUITANTIEME +HUITRIERS +HULULAIT +HULULATES +HULULERAIT +HULULIEZ +HUMAIENT +HUMANISAI +HUMANISER +HUMANISTE +HUMANOIDE +HUMATES +HUMECTAIS +HUMECTAT +HUMECTERAI +HUMECTERONT +HUMENT +HUMERAS +HUMEUR +HUMIDIFIENT +HUMIDIFUGES +HUMILIA +HUMILIAS +HUMILIE +HUMILIERAS +HUMILIIONS +HUMORALES +HUMUS +HUNTERS +HURDLEUSE +HURLANT +HURLAT +HURLERAI +HURLERONT +HURLUBERLU +HURONNES +HUSSARDE +HUTOISE +HYADES +HYALOIDES +HYBRIDAMES +HYBRIDATION +HYBRIDERAIS +HYBRIDEUR +HYBRIDITES +HYDATIQUE +HYDRANGEAS +HYDRARGIES +HYDRATAIENT +HYDRATASSENT +HYDRATEES +HYDRATEREZ +HYDRAULE +HYDRAVION +HYDRIQUE +HYDROCUTASSE +HYDROCUTENT +HYDROFUGEA +HYDROFUGERAI +HYDROGENAIS +HYDROGENAT +HYDROGENERAI +HYDROHEMIES +HYDROLOGIES +HYDROLYSAI +HYDROLYSERA +HYDRONYMIES +HYDROPHOBES +HYDROPIQUES +HYDROSPEEDS +HYDROZOAIRE +HYGIENE +HYGROMETRES +HYGROPHOBES +HYGROSTATS +HYLOZOISMES +HYMENS +HYOIDIENS +HYPERACTIFS +HYPERALGIES +HYPERBATES +HYPERBOREENS +HYPEREMIES +HYPERLIEN +HYPERONYMIES +HYPERPLANS +HYPERSONIQUE +HYPERTENDU +HYPERTENSIVE +HYPHES +HYPNOIDES +HYPNOTISANT +HYPNOTISEE +HYPOALGESIES +HYPOCAPNIES +HYPOCONDRE +HYPOCRAS +HYPOCYCLOIDE +HYPOGASTRE +HYPOIDES +HYPOLIPEMIE +HYPOMANIES +HYPONYMES +HYPOPHYSES +HYPOSODEE +HYPOSTASIAIT +HYPOSTASIE +HYPOSTASIONS +HYPOTENSIF +HYPOTHALAMUS +HYPOTHEQUAIS +HYPOXIQUE +HYSOPE +HYSTERISAS +HYSTERISEES +HYSTERISEREZ +IAKOUTE +IBERE +IBERIS +ICAQUES +ICEBERG +ICHTHYOSE +ICHTYOSAURE +ICONES +ICONOLATRES +ICONOLOGUES +ICOSAEDRAUX +IDE +IDEALISAMES +IDEALISATEUR +IDEALISER +IDEALISTE +IDEELLE +IDENTIFIABLE +IDENTIFIASSE +IDENTIFIES +IDENTITAIRE +IDEOLOGIE +IDEOMOTEURS +IDIOMATIQUE +IDIOTIFIA +IDIOTIFIERA +IDOINE +IDOLATRASSE +IDOLATRENT +IDOLATRERIEZ +IDOLATRIQUE +IDYLLIQUES +IGBOS +IGNAMES +IGNIFERES +IGNIFUGEANT +IGNIFUGEAT +IGNIFUGERAIS +IGNIFUGEZ +IGNITIONS +IGNOMINIE +IGNORAMES +IGNORANTISTE +IGNORATES +IGNORERAIT +IGNORIEZ +IGUES +ILE +ILEOCAECAUX +ILEUS +ILLEGALE +ILLICITEMENT +ILLIQUIDITES +ILLOCUTOIRES +ILLUMINAIT +ILLUMINATES +ILLUMINES +ILLUSIONNA +ILLUSIONNES +ILLUSOIRE +ILLUSTRAS +ILLUSTRATIF +ILLUSTREES +ILLUSTREREZ +ILLUTASSE +ILLUTEE +ILLUTERENT +ILLUTONS +ILLUVIUMS +ILOMBAS +ILOTISME +IMAGEAMES +IMAGEE +IMAGERAS +IMAGEUR +IMAGINA +IMAGINALE +IMAGINAT +IMAGINEE +IMAGINERENT +IMAGINONS +IMAM +IMBECILE +IMBIBAI +IMBIBASSIEZ +IMBIBERAI +IMBIBERONT +IMBITTABLE +IMBRIQUANT +IMBRIQUEE +IMBRIQUERENT +IMBRIQUONS +IMBUE +IMINES +IMITAMES +IMITATEUR +IMITE +IMITERAS +IMITIONS +IMMANENT +IMMANQUABLE +IMMATRICULE +IMMATURATION +IMMEDIATETE +IMMENSEMENT +IMMERGEAIS +IMMERGEAT +IMMERGERAIS +IMMERGEZ +IMMERSIONS +IMMIGRAIS +IMMIGRASSES +IMMIGRENT +IMMIGRERIEZ +IMMINENCES +IMMISCAMES +IMMISCE +IMMISCERAS +IMMISCIONS +IMMOBILISA +IMMOBILISER +IMMOBILISTES +IMMODESTE +IMMOLAMES +IMMOLATEUR +IMMOLER +IMMOLERIONS +IMMONDICE +IMMORALISTE +IMMORTEL +IMMUABLE +IMMUNISAIT +IMMUNISER +IMMUNITE +IMMUNOGENE +IMPACT +IMPACTANTS +IMPACTEE +IMPACTERENT +IMPACTIONS +IMPALAS +IMPANATIONS +IMPARIDIGITE +IMPARTIE +IMPARTIRENT +IMPARTISSANT +IMPARTITION +IMPASSIBLES +IMPATIENTAIT +IMPATIENTEZ +IMPATRONISAI +IMPAYEES +IMPECS +IMPEDIMENTA +IMPENSABLE +IMPERFECTIF +IMPERFOREES +IMPERIAUX +IMPERIUMS +IMPETRAI +IMPETRASSE +IMPETREE +IMPETRERENT +IMPETRONS +IMPIETES +IMPLACABLES +IMPLANTANT +IMPLANTERAIT +IMPLANTIEZ +IMPLEMENTAIT +IMPLEMENTIEZ +IMPLIQUA +IMPLIQUAS +IMPLIQUEES +IMPLIQUEREZ +IMPLORA +IMPLORAS +IMPLORE +IMPLORERAS +IMPLORIONS +IMPLOSASSE +IMPLOSERA +IMPLOSERONS +IMPLOSIVES +IMPOLIS +IMPOPULARITE +IMPORTAMES +IMPORTASSES +IMPORTERAIT +IMPORTIEZ +IMPORTUNAMES +IMPORTUNE +IMPORTUNIEZ +IMPOSAIENT +IMPOSASSENT +IMPOSER +IMPOSERIONS +IMPOSIONS +IMPOSTEUR +IMPOTENTS +IMPREGNA +IMPREGNASSE +IMPREGNEE +IMPREGNERENT +IMPREGNONS +IMPRESSIFS +IMPRIMANTE +IMPRIMATES +IMPRIMERAIS +IMPRIMERONT +IMPRO +IMPROMPTUE +IMPROS +IMPROUVAS +IMPROUVEES +IMPROUVEREZ +IMPROVISA +IMPROVISIEZ +IMPRUDENTS +IMPUDENT +IMPUDIQUES +IMPULSAIS +IMPULSAT +IMPULSERAIS +IMPULSEZ +IMPULSIVE +IMPUNIS +IMPURETES +IMPUTAIT +IMPUTATES +IMPUTERAIENT +IMPUTES +INABOUTI +INACCENTUE +INACCEPTEES +INACHEVEMENT +INACTIVAI +INACTIVER +INACTUALITE +INADAPTE +INALIENES +INALPAMES +INALPE +INALPERAS +INALPIONS +INAMENDABLE +INANITES +INAPERCUE +INAPPRECIEE +INAPPROPRIE +INARRETABLE +INATTENDUS +INAUGURAI +INAUGURASSE +INAUGURERAI +INAUGURERONT +INAVOUABLE +INCAPACITES +INCARCERAS +INCARCERE +INCARCERERAS +INCARCERIONS +INCARNAIT +INCARNATE +INCARNERA +INCARNERONS +INCASABLE +INCENDIAIRES +INCENDIES +INCERTAINES +INCHANGE +INCHAVIRABLE +INCIDEMMENT +INCINERAI +INCINEREES +INCINEREREZ +INCIPIT +INCISAIT +INCISATES +INCISERAIT +INCISIEZ +INCISURES +INCITANTS +INCITATEURS +INCITEE +INCITERENT +INCITONS +INCIVIQUE +INCLEMENTES +INCLINAIT +INCLINASSIEZ +INCLINER +INCLINERIONS +INCLUAIENT +INCLUIONS +INCLURENT +INCLUSIFS +INCLUSSIONS +INCOGNITOS +INCOLLABLES +INCOMBENT +INCOMBURANTS +INCOMMODANT +INCOMMODAT +INCOMMODES +INCOMMUTABLE +INCOMPETENTE +INCONDUITES +INCONGRUE +INCONSCIENTE +INCONSIDERE +INCONSTANCES +INCONTESTE +INCONVENANT +INCORPOREITE +INCORPORES +INCREMENTA +INCREMENTER +INCREMENTONS +INCRIMINAIT +INCRIMINATES +INCRIMINES +INCROYANCE +INCRUSTAIT +INCRUSTER +INCRUSTIONS +INCUBASSE +INCUBATIONS +INCUBERAIENT +INCUBES +INCULCATIONS +INCULPAS +INCULPE +INCULPERAS +INCULPIONS +INCULQUASSE +INCULQUENT +INCULQUERIEZ +INCULTES +INCUNABLES +INCURIEUX +INCURVAMES +INCURVATION +INCURVERAIS +INCURVEZ +INDAGUAIS +INDAGUAT +INDAGUERAS +INDAGUIONS +INDECISES +INDEFINI +INDELEBILE +INDEMNES +INDEMNISAS +INDEMNISE +INDEMNISERAS +INDEMNISIONS +INDENOUABLES +INDENTASSENT +INDENTEES +INDENTEREZ +INDEPASSABLE +INDEPENDANTS +INDETERMINES +INDEXA +INDEXANT +INDEXATIONS +INDEXERAIT +INDEXEURS +INDIANISAIS +INDIANISAT +INDIANISERAI +INDIANOLOGIE +INDICANS +INDICATEUR +INDICE +INDICERAS +INDICIAIRES +INDICONS +INDIFFERA +INDIFFERENTS +INDIGENATS +INDIGENTE +INDIGETES +INDIGNASSENT +INDIGNEES +INDIGNERENT +INDIGNITE +INDIGOTINES +INDIQUASSENT +INDIQUER +INDIQUERIONS +INDISCRET +INDISCUTEE +INDISPOSES +INDISSOLUBLE +INDIVIS +INDIVISIBLES +INDOL +INDOLENTS +INDOMPTEE +INDOU +INDRIENS +INDUCTIBLE +INDUCTRICE +INDUIRE +INDUISANT +INDUISISSE +INDUITES +INDULGENCIER +INDULINE +INDURAMES +INDURATION +INDURERAIS +INDUREZ +INDUSIES +INDUSTRIEUX +INECOUTABLES +INEDUCABLES +INEFFICACITE +INEGALEE +INELASTIQUES +INELIGIBLE +INEMPLOIS +INENTAMABLES +INEPROUVES +INEPUISEES +INERTA +INERTASSE +INERTENT +INERTERIEZ +INERTIELS +INESPERES +INETENDUE +INEXACTITUDE +INEXCITABLES +INEXECUTION +INEXIGIBLE +INEXORABLE +INEXPERTE +INEXPLOITES +INEXPRESSIFS +INFAMES +INFANTILE +INFANTS +INFARCIRAIT +INFARCISSAIS +INFARCIT +INFATUAIENT +INFATUERA +INFATUERONS +INFECONDITE +INFECTANT +INFECTAT +INFECTERAIS +INFECTEZ +INFECTIONS +INFEODAI +INFEODASSIEZ +INFEODER +INFEODERIONS +INFERAIENT +INFERASSIONS +INFERERENT +INFERNAL +INFERTILES +INFESTAS +INFESTE +INFESTERAS +INFESTIONS +INFIBULANT +INFIBULERAIT +INFIBULIEZ +INFIDELITE +INFILTRASSE +INFILTRE +INFILTRERAS +INFILTRIONS +INFINITES +INFINITUDES +INFIRMASSENT +INFIRMATIVE +INFIRMERAIS +INFIRMERONT +INFIRMITES +INFLAMMATION +INFLECHI +INFLECHIRAS +INFLEXIONS +INFLIGEASSE +INFLIGEONS +INFLIGERIEZ +INFLUA +INFLUASSES +INFLUENCABLE +INFLUENCER +INFLUENTES +INFLUERAS +INFLUIONS +INFOGRAPHIE +INFORMA +INFORMASSES +INFORMATIF +INFORMATISAT +INFORMEL +INFORMERAIT +INFORMIEZ +INFOROUTES +INFOUTUES +INFRADIENNES +INFRASONS +INFRUCTUEUSE +INFUSA +INFUSASSES +INFUSERA +INFUSERONS +INFUSIEZ +INGELIF +INGENIANT +INGENIEE +INGENIERENT +INGENIES +INGENIIEZ +INGENUMENT +INGERANT +INGEREE +INGERERAIT +INGERIEZ +INGRATE +INGRESSION +INGUINALES +INGURGITASSE +INGURGITEE +INGURGITONS +INHABITE +INHABITUELS +INHALANT +INHALATEURS +INHALERA +INHALERONS +INHARMONIES +INHERENTES +INHIBANTES +INHIBE +INHIBERAS +INHIBIONS +INHIBITOIRES +INHUMAIS +INHUMASSIEZ +INHUMER +INHUMERIONS +INIMITABLE +INIQUE +INITIAL +INITIALENT +INITIALERIEZ +INITIALISAT +INITIALISES +INITIASSENT +INITIATIQUE +INITIENT +INITIERIEZ +INJECTABLE +INJECTASSENT +INJECTER +INJECTERIONS +INJECTION +INJONCTIFS +INJURIAI +INJURIASSIEZ +INJURIERAI +INJURIERONT +INJUSTE +INJUSTIFIES +INNEE +INNERVAIENT +INNERVASSENT +INNERVEES +INNERVEREZ +INNES +INNOCENTAMES +INNOCENTE +INNOCENTERAS +INNOCENTIONS +INNOMES +INNOMMES +INNOVANTS +INNOVATEURS +INNOVERA +INNOVERONS +INNUS +INOBSERVES +INOCULABLE +INOCULASSENT +INOCULATRICE +INOCULERAIS +INOCULEZ +INOFFENSIF +INONDAIT +INONDATES +INONDERAIENT +INONDES +INOPERANTS +INOUBLIABLES +INQUIETAI +INQUIETASSE +INQUIETENT +INQUIETERIEZ +INQUIETUDE +INQUISITIF +INSALUBRITE +INSATIABLE +INSCRIPTION +INSCRIRIEZ +INSCRIVAIS +INSCRIVIEZ +INSCRIVIT +INSCULPAS +INSCULPEES +INSCULPEREZ +INSECABILITE +INSECTIFUGE +INSECURISANT +INSEMINAIENT +INSEMINEE +INSEMINERENT +INSEMINONS +INSERANT +INSEREE +INSERERENT +INSERMENTE +INSIDIEUX +INSINUAMES +INSINUERA +INSINUERONS +INSIPIDITES +INSISTANTE +INSISTATES +INSISTERENT +INSISTONS +INSOLAI +INSOLASSIEZ +INSOLEES +INSOLERAI +INSOLERONT +INSOMNIAQUE +INSONORES +INSONORISENT +INSORTABLE +INSOUCIEUX +INSOUPCONNE +INSPECTAIT +INSPECTATES +INSPECTERAIT +INSPECTEURS +INSPIRA +INSPIRAS +INSPIRATION +INSPIRER +INSPIRERIONS +INSTABLE +INSTALLASSE +INSTALLES +INSTANTANEE +INSTAURA +INSTAURASSES +INSTAURERAIT +INSTAURIEZ +INSTIGUA +INSTIGUASSES +INSTIGUERA +INSTIGUERONS +INSTILLAIS +INSTILLAT +INSTILLER +INSTINCTIFS +INSTINCTUELS +INSTITUANT +INSTITUEE +INSTITUERENT +INSTITUONS +INSTRUCTRICE +INSTRUIRIEZ +INSTRUISENT +INSTRUMENT +INSUFFISANTS +INSUFFLERAIS +INSUFFLEZ +INSULARITES +INSULTA +INSULTAS +INSULTEES +INSULTEREZ +INSULTEZ +INSUPPORTAIT +INSUPPORTE +INSUPPORTONS +INSURGEASSE +INSURGEONS +INSURGERIEZ +INTACTS +INTAILLER +INTANGIBLE +INTEGRAIENT +INTEGRANT +INTEGRASSIEZ +INTEGRATIVE +INTEGRER +INTEGRERIONS +INTEGRISTES +INTELLOCRATE +INTEMPERIES +INTEMPORELS +INTENSEMENT +INTENSIFIAS +INTENSIFIEES +INTENSION +INTENSIVES +INTENTASSENT +INTENTER +INTENTERIONS +INTERACTIFS +INTERAGIRAIS +INTERALLIES +INTERCALAIRE +INTERCALER +INTERCEDASSE +INTERCEDERA +INTERCEPTAT +INTERCURRENT +INTERDICTION +INTERDIRAIS +INTERDIS +INTERDISIONS +INTERDITS +INTERESSANTS +INTERESSEE +INTERESSIEZ +INTERFACAI +INTERFACERAI +INTERFECOND +INTERFERA +INTERFERERAS +INTERFERIONS +INTERFOLIAGE +INTERFOLIERA +INTERIEURES +INTERJETAI +INTERJETEZ +INTERLIGNAGE +INTERLIGNERA +INTERLOPE +INTERLOQUER +INTERMEZZOS +INTERNAIENT +INTERNALISAS +INTERNALISER +INTERNASSENT +INTERNATS +INTERNENT +INTERNERIEZ +INTERNISTE +INTEROSSEUSE +INTERPELAIS +INTERPELAT +INTERPELIONS +INTERPELLES +INTERPOLEZ +INTERPOSANT +INTERPOSEE +INTERPOSITIF +INTERPRETAIS +INTERPRETERA +INTERROGATIF +INTERROGE +INTERROGEAS +INTERROGEONS +INTERROMPRA +INTERSECTES +INTERSEXUEL +INTERSIGNES +INTERURBAINE +INTERVENANT +INTERVIEWEE +INTERZONALES +INTESTINAUX +INTIMAIS +INTIMAT +INTIMERA +INTIMERONS +INTIMIDAIT +INTIMIDE +INTIMIDERAS +INTIMIDIONS +INTIMONS +INTITULAIENT +INTITULES +INTOLERANT +INTONATIVE +INTOXICANTE +INTOXIQUAMES +INTOXIQUE +INTOXIQUERAS +INTOXIQUIONS +INTRAITABLE +INTREPIDITES +INTRIGUAIS +INTRIGUAT +INTRIGUERAIS +INTRIGUEZ +INTRIQUAIS +INTRIQUAT +INTRIQUERAIS +INTRIQUEZ +INTRODUIRAS +INTRODUISAIT +INTRODUISIS +INTRODUITES +INTRONISAI +INTRONISER +INTRORSES +INTROSPECTEZ +INTROUVABLES +INTRUSIF +INTUBAIT +INTUBATES +INTUBERAIENT +INTUBES +INTUITAMES +INTUITE +INTUITERAS +INTUITIF +INTUITIVES +INUITE +INUKTITUTS +INUSITES +INVAGINAIENT +INVAGINERA +INVAGINERONS +INVAINCUS +INVALIDANTS +INVALIDERAIT +INVALIDIEZ +INVARIABLES +INVASION +INVECTIVANT +INVECTIVEE +INVECTIVONS +INVENGES +INVENTAS +INVENTEES +INVENTEREZ +INVENTIF +INVENTORIAGE +INVENTORIERA +INVERIFIE +INVERSAIT +INVERSATES +INVERSERAIS +INVERSEUR +INVERSIVE +INVERTIE +INVERTIRAIT +INVERTISSAIS +INVERTIT +INVESTIGUERA +INVESTIS +INVESTISSEUR +INVESTITURE +INVETERASSE +INVETERENT +INVETERERIEZ +INVIOLEE +INVITAIENT +INVITASSENT +INVITE +INVITERAS +INVITEUSE +INVOCATION +INVOLUCRE +INVOLUTION +INVOQUANT +INVOQUEE +INVOQUERENT +INVOQUONS +IODAIT +IODATE +IODERAIS +IODEUSE +IODISME +IODLASSE +IODLERA +IODLERONS +IODLONS +IONIENNE +IONISAMES +IONISASSIONS +IONISERA +IONISERONS +IONISONS +IOULAMES +IOULE +IOULEREZ +IOURTE +IPOMEE +IQALUMMIUQ +IRAKIENS +IRAQUIENNE +IRENIQUES +IRIDEES +IRIDIENNES +IRIDOLOGUES +IRISABLES +IRISASSES +IRISENT +IRISERIEZ +IRLANDAIS +IRONIQUEMENT +IRONISASSE +IRONISERA +IRONISERONS +IRONT +IRRACHETABLE +IRRADIANT +IRRADIAT +IRRADIER +IRRADIERIONS +IRRAISONNE +IRRATIONNEL +IRREALISEES +IRRECEVABLE +IRREFLECHI +IRREFRENABLE +IRREFUTES +IRRELIGIEUX +IRRESOLUTION +IRRIGATION +IRRIGUANT +IRRIGUEE +IRRIGUERENT +IRRIGUONS +IRRITAMES +IRRITASSIONS +IRRITEE +IRRITERENT +IRRITONS +ISARDS +ISCHEMIQUE +ISERANES +ISLAMISA +ISLAMISAS +ISLAMISE +ISLAMISERAS +ISLAMISIONS +ISLANDAISE +ISMAILIEN +ISOBATHES +ISOCELES +ISOCLINIQUES +ISOEDRIQUES +ISOGAMIES +ISOGONES +ISOIONIQUES +ISOLANTE +ISOLATES +ISOLATRICES +ISOLERAI +ISOLERONT +ISOLOIRS +ISOMERISA +ISOMERISENT +ISOMETRIES +ISONIAZIDES +ISOPHASES +ISORELS +ISOSTATIQUES +ISOTOPIES +ISRAELIENNE +ISSAS +ISTREENNE +ITALIANISANT +ITALIANISEZ +ITALIENNE +ITERAIS +ITERAT +ITEREE +ITERERENT +ITERONS +ITINERANTES +IVOIRERIE +IVOIRIERS +IVRES +IVROGNESSES +IXAMES +IXE +IXERAS +IXIAS +JABIRUS +JABLAS +JABLEES +JABLEREZ +JABLIERES +JABOT +JABOTAS +JABOTER +JABOTERIONS +JABOTIONS +JACARANDA +JACASSASSE +JACASSENT +JACASSERIE +JACASSEUSES +JACEES +JACISTES +JACOBEE +JACOBINISMES +JACQUARDE +JACQUERIES +JACTAIENT +JACTASSES +JACTERA +JACTERONS +JACTONS +JAGUAR +JAILLIRAI +JAILLIRONT +JAIN +JAKARTANAISE +JALONNAIS +JALONNAT +JALONNERAI +JALONNERONT +JALONS +JALOUSASSENT +JALOUSENT +JALOUSERIEZ +JALOUSONS +JAMAIQUAINS +JAMBEES +JAMBONNEAUX +JAMEROSE +JANOTISME +JANTHINES +JAPONAIS +JAPONISAIS +JAPONISASSES +JAPONISERA +JAPONISERONS +JAPONISTES +JAPPASSE +JAPPENT +JAPPERIEZ +JAPPIEZ +JAQUES +JARDES +JARDINANT +JARDINEE +JARDINERENT +JARDINETS +JARDINIONS +JARGONNERAI +JARGONNERONT +JARGONNIEZ +JAROUSSES +JARRETAIT +JARRETATES +JARRETIERE +JARS +JARTASSENT +JARTER +JARTERIONS +JASAI +JASASSE +JASEMENT +JASERANS +JASETTE +JASIONS +JASPAIENT +JASPASSIONS +JASPERAIENT +JASPES +JASPINAS +JASPINEES +JASPINEREZ +JASPIONS +JASSAMES +JASSE +JASSEREZ +JASSEZ +JATTEES +JAUGEAMES +JAUGEE +JAUGERAS +JAUGEUSE +JAUNATRES +JAUNIMES +JAUNIRIEZ +JAUNISSANT +JAUNISSIEZ +JAVARTS +JAVELAI +JAVELASSIEZ +JAVELEUR +JAVELLENT +JAVELLERONS +JAVELLISAMES +JAVELLISERA +JAVOTTE +JAZZMANS +JEANNOTISME +JEJUNALE +JENNYS +JERKAMES +JERKE +JERKEREZ +JERKS +JERRICANS +JESUITE +JETABLE +JETAS +JETEES +JETIONS +JETTE +JETTERIONS +JEUNAIENT +JEUNASSIONS +JEUNERAIS +JEUNESSE +JEUNEZ +JEUNOTTE +JIHADISTE +JIVAROS +JOBARD +JOBARDASSENT +JOBARDER +JOBARDERIES +JOBARDISES +JOCASSE +JOCRISSE +JODLAIT +JODLATES +JODLERENT +JODLEUSES +JOGGAMES +JOGGE +JOGGEREZ +JOGGEUSES +JOHANNIQUE +JOIGNABILITE +JOIGNES +JOIGNISSIEZ +JOINDRAS +JOINTAGE +JOINTASSENT +JOINTEMENTS +JOINTEREZ +JOINTIONS +JOINTONS +JOINTOYERENT +JOINTS +JOKERS +JOLIETTAINES +JONAGOLD +JONCAMES +JONCE +JONCERAS +JONCHAI +JONCHASSENT +JONCHEMENTS +JONCHERAS +JONCHET +JONCTION +JONGLANT +JONGLENT +JONGLERIE +JONGLEUSES +JONQUILLES +JOSEPHISME +JOUABILITE +JOUAILLAIT +JOUAILLATES +JOUAILLERENT +JOUAILLONS +JOUASSE +JOUE +JOUERAS +JOUET +JOUFFLUE +JOUIRA +JOUIRONS +JOUISSANTES +JOUISSIEZ +JOUJOUTHEQUE +JOURNALEUSE +JOURNALISAIS +JOURNEES +JOUTAS +JOUTER +JOUTERIONS +JOUTIONS +JOUXTAIENT +JOUXTASSIONS +JOUXTERAIENT +JOUXTES +JOVIALITES +JOYEUSEMENT +JUBES +JUBILANTE +JUBILATES +JUBILERAIENT +JUBILES +JUCHAMES +JUCHE +JUCHERAS +JUCHIONS +JUDAISAIENT +JUDAISASSENT +JUDAISEES +JUDAISEREZ +JUDAISMES +JUDEENS +JUDICATURES +JUDICIEUX +JUGALE +JUGEAIT +JUGEATES +JUGERA +JUGERONS +JUGLANDACEE +JUGULAIT +JUGULATES +JUGULERAIT +JUGULIEZ +JUINS +JUJUBIERS +JULIENS +JUMEL +JUMELAS +JUMELEES +JUMELLERAI +JUMELLES +JUMPA +JUMPASSES +JUMPERAIENT +JUMPERS +JUNCOS +JUNKER +JUNONIENS +JUPIERS +JUPONNAIENT +JUPONNES +JURAIENT +JURASSE +JURASSIQUES +JUREMENTS +JUREREZ +JUREZ +JURIEZ +JURONS +JUSQUIAME +JUSSION +JUSTICE +JUSTIFIA +JUSTIFIANTES +JUSTIFIE +JUSTIFIERAS +JUSTIFIIONS +JUTASSE +JUTERA +JUTERONS +JUVENAT +JUXTAPOSERA +KABBALISTE +KABOULI +KABYE +KADDISH +KAGOU +KAKAWI +KALE +KALIS +KAMALAS +KAMPALAISE +KANANGAIS +KANGLAR +KANS +KAOLACKOISE +KAOLINISAIS +KAOLINISAT +KAOLINISERAI +KAONS +KARAKUL +KARBAUX +KARMAN +KARSTIQUE +KASAIENNES +KATAKANA +KATHAK +KAWAS +KAYAKS +KEBABS +KEFTAS +KEMALISTES +KENS +KENYANE +KEPIS +KERATINISA +KERATINISER +KERATIQUES +KERN +KET +KEUM +KEYNESIENNES +KHALIFES +KHANATS +KHEDIVES +KHMERES +KHOLS +KIBITZAIT +KIBITZATES +KIBITZERAIT +KIBITZIEZ +KICKS +KIDNAPPANT +KIDNAPPEE +KIDNAPPERENT +KIDNAPPEUSES +KIESELGUHR +KIF +KIFASSENT +KIFER +KIFERIONS +KIFFAMES +KIFFE +KIFFERAS +KIFFIONS +KIKI +KILOBASE +KILOEURO +KILOHMS +KILOMETRAMES +KILOMETRE +KILOMETRERAS +KILOMETRIONS +KILOTONNES +KILOWATTS +KIMMERIDGIEN +KINE +KINKAJOU +KIOSQUES +KIPPER +KIRIBATIENNE +KIT +KIWI +KLAXONNAS +KLAXONNEES +KLAXONNEREZ +KLAXONS +KLEZMER +KNESSET +KOALAS +KODAKS +KOIS +KOLKHOZE +KOMSOMOL +KONZERN +KORAS +KORRIGANE +KOTAI +KOTASSIEZ +KOTERAIS +KOTEUR +KOTS +KOUFFAS +KOUPREYS +KOWEITIENS +KRAKEN +KREMLINS +KRILL +KRYPTON +KUFIQUE +KUNDALINI +KWACHA +KYMOGRAPHES +KYSTIQUE +LABARUM +LABELISAIS +LABELISAT +LABELISERAI +LABELISERONT +LABELLISERA +LABFERMENT +LABIALISAIS +LABIALISAT +LABIALISERAI +LABIES +LABORATOIRE +LABOURABLES +LABOURASSE +LABOURENT +LABOURERIEZ +LABOURONS +LABRE +LABYRINTHE +LACAIS +LACASSENT +LACCOLITE +LACEES +LACERAMES +LACERATION +LACERERAIS +LACEREZ +LACERTIENS +LACEZ +LACHAS +LACHEES +LACHERENT +LACHEUR +LACINIEE +LACONIENS +LACRYMALE +LACTALBUMINE +LACTATION +LACTESCENTES +LACTODUCS +LACTOSERUM +LACUNEUSES +LADANUMS +LADITE +LAETARE +LAGOPEDE +LAGUNAGE +LAICARD +LAICISAIENT +LAICISERA +LAICISERONS +LAICISTES +LAIDERONS +LAIERAIS +LAIMARGUES +LAINAS +LAINEES +LAINEREZ +LAINEURS +LAINIONS +LAISSAIS +LAISSAT +LAISSERAIS +LAISSEZ +LAITEE +LAITIER +LAITONNAIS +LAITONNAT +LAITONNERAIS +LAITONNEZ +LAIUSSAI +LAIUSSASSIEZ +LAIUSSERAIS +LAIUSSEUR +LAKH +LAMAGES +LAMAIT +LAMARCKIENNE +LAMASERIE +LAMBA +LAMBEAU +LAMBINA +LAMBINASSE +LAMBINERA +LAMBINERONS +LAMBLIAS +LAMBRISSAGES +LAMBRISSERA +LAMBRUSCOS +LAMELLAIRES +LAMENT +LAMENTANT +LAMENTATIONS +LAMENTERAIT +LAMENTIEZ +LAMERAIT +LAMIACEE +LAMIFIEE +LAMINAIS +LAMINAT +LAMINERAI +LAMINERIONS +LAMINIEZ +LAMPADOPHORE +LAMPANTS +LAMPASSIONS +LAMPERA +LAMPERON +LAMPISTE +LAMPROIES +LANAUDOISE +LANCANT +LANCEE +LANCERA +LANCERONS +LANCEZ +LANCINANT +LANCINAT +LANCINER +LANCINERIONS +LANCONS +LANDAUS +LANDIERS +LANDSTURM +LANGAGIER +LANGEAMES +LANGEE +LANGERAS +LANGHIENNE +LANGOUSTES +LANGUEYA +LANGUEYASSE +LANGUEYENT +LANGUEYERIEZ +LANGUIDE +LANGUIRAIENT +LANGUIS +LANGUISSENT +LANICE +LANIIDE +LANTANAS +LANTERNAS +LANTERNEAUX +LANTERNERAS +LANTERNEZ +LANTHANIDES +LAOBES +LAOTIENS +LAPAMES +LAPASSENT +LAPEMENTS +LAPEREAUX +LAPICIDE +LAPIDANT +LAPIDATIONS +LAPIDERAIT +LAPIDEURS +LAPIDIFIAMES +LAPIDIFIEZ +LAPILLIS +LAPINASSE +LAPINERA +LAPINERONS +LAPINONS +LAPONES +LAPSUS +LAQUAMES +LAQUE +LAQUERAIT +LAQUEURS +LARBIN +LARDAGE +LARDASSENT +LARDER +LARDERIONS +LARDONNA +LARDONNASSES +LARDONNERA +LARDONNERONS +LARES +LARGET +LARGUA +LARGUASSES +LARGUERA +LARGUERONS +LARIDE +LARMIER +LARMOIERAIT +LARMOYAIENT +LARMOYASSENT +LARMOYEURS +LARRONNESSE +LARVES +LARYNGOLOGIE +LARYNGOSCOPE +LASCAR +LASCIVITES +LASSANTE +LASSATES +LASSERAIT +LASSES +LASSOS +LASURAIT +LASURATES +LASURERAIT +LASURIEZ +LATENTS +LATERALISES +LATERITIQUES +LATIF +LATINISAIENT +LATINISEES +LATINISEREZ +LATINISMES +LATITUDES +LATRINES +LATTAIT +LATTATES +LATTERAIT +LATTIEZ +LAUDATEUR +LAUDIENNE +LAUREE +LAURIERS +LAVABILITES +LAVAGES +LAVAMES +LAVANTE +LAVASSIONS +LAVEES +LAVERAS +LAVETTE +LAVIONS +LAVURES +LAXISMES +LAYAIS +LAYAT +LAYERAIS +LAYETIER +LAYONS +LAZURITES +LEADERSHIPS +LECANORES +LECHAS +LECHEES +LECHERAIS +LECHERONT +LECHOUILLA +LECHOUILLES +LECONS +LECTISTERNES +LEDIT +LEGALISA +LEGALISASSES +LEGALISENT +LEGALISERIEZ +LEGALISONS +LEGATO +LEGENDAIS +LEGENDAT +LEGENDERAIS +LEGENDEZ +LEGERS +LEGIFERAIT +LEGIFERATES +LEGIFERERENT +LEGIFERONS +LEGIONNAIRE +LEGITIMAIENT +LEGITIMASSES +LEGITIMEMENT +LEGITIMEREZ +LEGITIMISMES +LEGUAI +LEGUASSIEZ +LEGUERAI +LEGUERONT +LEGUMIERS +LEIBNIZIEN +LEIPZIGOISES +LEK +LEMMATISAMES +LEMMATISEZ +LEMNISCATE +LEMURIEN +LENIFIAIENT +LENIFIASSENT +LENIFIER +LENIFIERIONS +LENINISTE +LENTE +LENTICULEE +LENTILLES +LENTS +LEONE +LEONTODON +LEOTARD +LEPISME +LEPREUX +LEPROME +LEPTOCEPHALE +LEPTOSOME +LEPTYNITE +LESAIENT +LESASSIONS +LESBISMES +LESERAIENT +LESES +LESINAS +LESINER +LESINERIES +LESINEZ +LESIONNAIS +LESIONNAT +LESIONNER +LESOTHAN +LESSIVAGES +LESSIVASSES +LESSIVERA +LESSIVERONS +LESSIVIELLES +LESTAGES +LESTASSES +LESTER +LESTERIONS +LETAL +LETHARGIQUE +LETTONES +LETTRAIT +LETTRATES +LETTRERAIT +LETTREURS +LETTRISMES +LEUCEMIQUES +LEUCOMA +LEUCOPOIESE +LEUDE +LEURRAS +LEURREES +LEURREREZ +LEURS +LEVAIS +LEVANTIN +LEVASSIONS +LEVERAIENT +LEVERS +LEVIGATION +LEVIGEAS +LEVIGENT +LEVIGEREZ +LEVIRAT +LEVITAI +LEVITASSIEZ +LEVITERAI +LEVITERONT +LEVONS +LEVRETTAIS +LEVRETTAT +LEVRETTERAIS +LEVRETTEZ +LEVS +LEVURAMES +LEVURE +LEVURERAS +LEVURIERS +LEXICALISAI +LEXICALISERA +LEXIQUE +LEZARDAMES +LEZARDE +LEZARDERAS +LEZARDIONS +LIAISON +LIAISONNER +LIAMES +LIANTS +LIARDASSE +LIARDERA +LIARDERONS +LIASIQUES +LIBANAIS +LIBELLAI +LIBELLASSIEZ +LIBELLERAI +LIBELLERONT +LIBER +LIBERALEMENT +LIBERALISEES +LIBERALISONS +LIBERASSIONS +LIBERATRICES +LIBERERAIS +LIBEREZ +LIBERISTES +LIBERTE +LIBERTY +LIBIDOS +LIBREMENT +LIBYEN +LICENCES +LICENCIAS +LICENCIEES +LICENCIERAS +LICENCIEUSES +LICHAIS +LICHAT +LICHENT +LICHERIEZ +LICHEUSES +LICITAI +LICITASSIEZ +LICITEES +LICITERENT +LICITONS +LIDARS +LIEES +LIEGEASSE +LIEGEOIS +LIEGERAS +LIEGEUSE +LIENT +LIERIEZ +LIESSES +LIEUSES +LIEZ +LIFTASSE +LIFTENT +LIFTERIEZ +LIFTIER +LIGAMENT +LIGASES +LIGATURER +LIGERIENNE +LIGNAGERE +LIGNARD +LIGNE +LIGNERAS +LIGNETTE +LIGNICOLE +LIGNIFIAS +LIGNIFIE +LIGNIFIERAS +LIGNIFIIONS +LIGNOMETRES +LIGOTAMES +LIGOTE +LIGOTERAS +LIGOTIONS +LIGUAS +LIGUEES +LIGUEREZ +LIGUEZ +LIGURE +LILAS +LIMACE +LIMAIS +LIMASSENT +LIMBE +LIMEE +LIMERA +LIMERIEZ +LIMEURS +LIMINAIRES +LIMITAIENT +LIMITASSENT +LIMITATIVE +LIMITERAIENT +LIMITES +LIMIVORES +LIMNIQUES +LIMOGEAIENT +LIMOGERAIENT +LIMOGES +LIMONADIERES +LIMONAMES +LIMONE +LIMONERAIS +LIMONEUSE +LIMONITE +LIMOUSIN +LIMOUSINAS +LIMOUSINEES +LIMOUSINEREZ +LIMOUSINS +LINAIGRETTE +LINDOR +LINERS +LINGER +LINGOTA +LINGOTASSES +LINGOTERA +LINGOTERONS +LINGOTONS +LINGUET +LINGUISTIQUE +LINKAGE +LINOLEINE +LINOTTES +LINSOIR +LIONNES +LIPIDEMIE +LIPOCHROMES +LIPOIDIQUE +LIPOPHILE +LIPOSOLUBLE +LIPOSUCANT +LIPOSUCCIONS +LIPOSUCERAIT +LIPOSUCIEZ +LIPPEE +LIQUEFIAI +LIQUEFIASSE +LIQUEFIENT +LIQUEFIERIEZ +LIQUETTES +LIQUIDAMBAR +LIQUIDAT +LIQUIDATRICE +LIQUIDERAIS +LIQUIDEZ +LIQUOREUSE +LIRAS +LIRONT +LISBONNAIS +LISERAGE +LISERASSENT +LISERER +LISERERIONS +LISETTE +LISIBLEMENT +LISPS +LISSANTE +LISSATES +LISSERAIT +LISSEURS +LISSOIR +LISTAMES +LISTE +LISTERAI +LISTERIONS +LISTIONS +LITAIENT +LITASSE +LITEAU +LITERAIT +LITES +LITHIASIQUE +LITHIUM +LITHOGENES +LITHOPHANIES +LITHOTRITEUR +LITIEZ +LITIONS +LITRAGES +LITRASSES +LITRERA +LITRERONS +LITSAMS +LITTERARITE +LITTORALE +LITURGIES +LIVAROT +LIVIDES +LIVRABLES +LIVRASSE +LIVRENT +LIVRERIEZ +LIVREURS +LIXIVIAIS +LIXIVIAT +LIXIVIERA +LIXIVIERONS +LLANOS +LOBAIENT +LOBASSES +LOBBYISMES +LOBECTOMIES +LOBER +LOBERIONS +LOBIS +LOBOTOMISANT +LOBOTOMISEES +LOBULAIRE +LOCALE +LOCALISAIS +LOCALISAT +LOCALISEES +LOCALISEREZ +LOCALISMES +LOCATIF +LOCAVORES +LOCHASSE +LOCHENT +LOCHERIEZ +LOCHS +LOCKOUTAMES +LOCKOUTE +LOCKOUTERAS +LOCKOUTIONS +LOCOMOTION +LOCOS +LOCULEUSES +LOCUTIONS +LOESS +LOFANT +LOFENT +LOFERIEZ +LOFT +LOGATOMES +LOGEANT +LOGEES +LOGERAIT +LOGETTES +LOGICIELLES +LOGIGRAMMES +LOGISTE +LOGO +LOGOGRIPHES +LOGOPEDIES +LOGOTHETE +LOGUAMES +LOGUE +LOGUERAS +LOGUIONS +LOIRS +LOLITA +LOMBALGIE +LOMBES +LOMBRICOIDE +LOMEENNE +LONDONIENS +LONGANIMITES +LONGEANT +LONGEES +LONGERAIT +LONGERONT +LONGIMETRIE +LONGUEMENT +LONGUEURS +LOOKS +LOPINS +LOQUETAIENT +LOQUETEUX +LORAN +LORETTES +LORGNASSENT +LORGNER +LORGNERIONS +LORGNEZ +LORIENTAISES +LORRAIN +LOSANGE +LOTERIE +LOTIONNA +LOTIONNASSES +LOTIONNERA +LOTIONNERONS +LOTIRAI +LOTIRONT +LOTISSES +LOTITES +LOTTES +LOUAIT +LOUANGEAS +LOUANGENT +LOUANGEREZ +LOUANGEZ +LOUAT +LOUBINE +LOUCHAS +LOUCHEBEMES +LOUCHERAIS +LOUCHERIONS +LOUCHEZ +LOUCHIRAIT +LOUCHISSAIS +LOUCHIT +LOUER +LOUERIONS +LOUFIAT +LOUGRES +LOUKOUMS +LOUPAGE +LOUPASSENT +LOUPER +LOUPERIONS +LOUPIOTE +LOURAIS +LOURAT +LOURDAMES +LOURDAUD +LOURDERA +LOURDERIEZ +LOURDINGUES +LOURER +LOURERIONS +LOUSTIC +LOUVAIENT +LOUVASSES +LOUVERA +LOUVERONS +LOUVETANT +LOUVETEAU +LOUVETIONS +LOUVETTERAS +LOUVIEROISES +LOUVOIERAIS +LOUVOYA +LOUVOYASSE +LOUVOYEZ +LOVANT +LOVEE +LOVERAIT +LOVIEZ +LOYALES +LOZERIEN +LUBRICITE +LUBRIFIANTES +LUBRIFIER +LUBRIQUES +LUCANISTES +LUCIDE +LUCIFERIENS +LUCIOLES +LUCRATIVES +LUDIONS +LUDOLOGUES +LUES +LUGEA +LUGEASSES +LUGERA +LUGERONS +LUGIONS +LUIRAIT +LUISAIENT +LUISENT +LUISISSE +LULU +LUMEN +LUMINANCES +LUMINEUX +LUMINOSITES +LUNAISONS +LUNCHAIT +LUNCHATES +LUNCHERENT +LUNCHONS +LUNETIERES +LUNETTIERES +LUO +LUPIQUES +LUREX +LUSHOISE +LUSTRAGE +LUSTRANT +LUSTRAT +LUSTRERA +LUSTRERIEZ +LUSTRIEZ +LUTA +LUTASSE +LUTEALES +LUTEINE +LUTENT +LUTERIEZ +LUTETIUMS +LUTHERIES +LUTINA +LUTINASSES +LUTINERA +LUTINERIEZ +LUTIONS +LUTTAIS +LUTTAT +LUTTERAS +LUTTEUSE +LUXAIENT +LUXASSIONS +LUXERENT +LUXMETRE +LUXURIANCES +LUZERNIERE +LYCAON +LYCENIDE +LYCOPENE +LYDIENNE +LYMPHATIQUES +LYMPHOCYTE +LYMPHOKINES +LYNCHAGE +LYNCHASSENT +LYNCHER +LYNCHERIONS +LYNCHIONS +LYOPHILISAT +LYOPHILISENT +LYRA +LYRASSES +LYRERAIENT +LYRES +LYRISMES +LYSAS +LYSEE +LYSERENT +LYSERONS +LYSONS +LYSOSOMIAUX +MABOULS +MACADAMISA +MACADAMISER +MACANAISES +MACARONADES +MACCARTHYSME +MACCHIAIOLI +MACERAIS +MACERAT +MACEREES +MACEREREZ +MACERONS +MACHANT +MACHATES +MACHERA +MACHERONS +MACHIAVEL +MACHICOT +MACHICOTAS +MACHICOTER +MACHIEZ +MACHINALES +MACHINATES +MACHINEES +MACHINEREZ +MACHINIONS +MACHISME +MACHOIRONS +MACHONNASSE +MACHONNEMENT +MACHONNERENT +MACHONNONS +MACHOUILLAS +MACHOUILLENT +MACHURAIENT +MACHURES +MACLAGES +MACLASSES +MACLERA +MACLERONS +MACONNAGES +MACONNASSE +MACONNENT +MACONNERIE +MACONNIQUE +MACQUAIENT +MACQUASSIONS +MACQUERAIENT +MACQUES +MACREUSES +MACROCHEIRE +MACROURES +MACULAIRES +MACULASSIONS +MACULENT +MACULERIEZ +MACULIONS +MADECASSE +MADELINOTES +MADERIENS +MADERISEES +MADERISEREZ +MADICOLE +MADRAGUES +MADREPORES +MADRIGAL +MAERL +MAFFES +MAFFLUE +MAFIOSOS +MAGANASSENT +MAGANER +MAGANERIONS +MAGASINAGE +MAGASINER +MAGASINIERE +MAGHREBINS +MAGIQUEMENT +MAGISTRAT +MAGMATIQUES +MAGNANARELLE +MAGNANIMES +MAGNASSES +MAGNER +MAGNERIONS +MAGNETISABLE +MAGNETISANTS +MAGNETISEURS +MAGNETITES +MAGNETOS +MAGNIFIAIENT +MAGNIFIE +MAGNIFIERAS +MAGNIFIIONS +MAGNOLIACEES +MAGOTS +MAGOUILLAS +MAGOUILLEES +MAGOUILLEREZ +MAGOUILLEZ +MAHALEB +MAHARANE +MAHDISME +MAHONIA +MAHRATTE +MAIEURE +MAIGRELET +MAIGRICHONNE +MAIGRIR +MAIGRIRIONS +MAIGRISSES +MAILLA +MAILLANTES +MAILLE +MAILLERAIS +MAILLET +MAILLETAS +MAILLETEES +MAILLETTE +MAILLOCHES +MAIN +MAINTENANCES +MAINTENUE +MAINTIENDRAS +MAINTIENT +MAINTINTES +MAIRE +MAISICOLES +MAISONNERIE +MAITRES +MAITRISAMES +MAITRISERAI +MAITRISERONT +MAJESTE +MAJOLIQUE +MAJORAL +MAJORASSIEZ +MAJORE +MAJORERAS +MAJOREZ +MAJORQUINE +MAKHZEN +MAKOSSA +MALABEENNE +MALACOLOGIES +MALADIE +MALADRESSES +MALAIMEES +MALAISIEN +MALANDREUX +MALARIA +MALAWIEN +MALAXAIENT +MALAXASSIONS +MALAXERA +MALAXERONS +MALAYALAMS +MALBATIE +MALDIVIENNES +MALEFICES +MALEKITES +MALENTENDU +MALFAISANT +MALFORMATIF +MALGRACIEUSE +MALHONNETETE +MALIENNE +MALIKITE +MALINKES +MALIS +MALLES +MALMENA +MALMENASSE +MALMENENT +MALMENERIEZ +MALMIGNATTES +MALOLACTIQUE +MALOUF +MALPIGHIE +MALPOSITION +MALSAINES +MALSONNANTES +MALTAIS +MALTASSENT +MALTER +MALTERIES +MALTOSE +MALTRAITAIT +MALTRAITER +MALURES +MALVOYANCE +MAMAMOUCHIS +MAMEES +MAMELONS +MAMELUKES +MAMMA +MAMMITE +MAMMY +MANADIERS +MANAGEASSE +MANAGEMENTS +MANAGERENT +MANAGERS +MANAGUAYENNE +MANAT +MANCHE +MANCHONNA +MANCHONNERA +MANCHOTES +MANDA +MANDANT +MANDARINAUX +MANDASSIEZ +MANDATAMES +MANDATE +MANDATERAIS +MANDATEZ +MANDCHOUS +MANDEMENT +MANDERENT +MANDIBULATE +MANDOLE +MANDORLES +MANDRINAIENT +MANDRINES +MANEGE +MANEGEASSENT +MANEGER +MANEGERIONS +MANETTE +MANGANESES +MANGANIQUE +MANGEAIENT +MANGEASSES +MANGEONS +MANGEOTER +MANGEOTTES +MANGERAIT +MANGERS +MANGEZ +MANGOS +MANGOUSTIERS +MANIABILITE +MANIAMES +MANIASSIEZ +MANICHORDION +MANIEMENTS +MANIEREE +MANIERISTES +MANIFESTA +MANIFESTAS +MANIFESTE +MANIFESTIEZ +MANIGANCAIT +MANIGANCATES +MANIGANCIEZ +MANILLAIS +MANILLASSIEZ +MANILLERAI +MANILLERONT +MANILLONS +MANIPULABLES +MANIPULASSES +MANIPULERAIT +MANIPULIEZ +MANITOBAINS +MANNEQUINATS +MANNOISE +MANOEUVRABLE +MANOEUVRER +MANOEUVRIONS +MANOMETRIQUE +MANOQUAIT +MANOQUATES +MANOQUERAIT +MANOQUIEZ +MANOUCHE +MANQUAIS +MANQUASSES +MANQUENT +MANQUERIEZ +MANSA +MANSUETUDE +MANTELE +MANTELURE +MANTOUANE +MANUCURAI +MANUCURERAI +MANUCURERONT +MANUELINE +MANUFACTURAI +MANUSCRITS +MANUTERGE +MAOISTE +MAPPA +MAPPASSE +MAPPEMONDE +MAPPERENT +MAPPONS +MAQUAIENT +MAQUASSIONS +MAQUERAIENT +MAQUEREAUTEE +MAQUERELLA +MAQUERELLES +MAQUERONT +MAQUETTANT +MAQUETTEE +MAQUETTERENT +MAQUETTISME +MAQUIGNONNAT +MAQUILLAMES +MAQUILLE +MAQUILLERAS +MAQUILLEUSE +MAQUISARDES +MARABOUTAIS +MARABOUTAT +MARABOUTEZ +MARACAS +MARAICHER +MARANTA +MARASQUES +MARAUDAIENT +MARAUDES +MARAVEDIS +MARBRASSENT +MARBRER +MARBRERIES +MARBREZ +MARC +MARCASSITES +MARCESCENTS +MARCHANDA +MARCHANDASSE +MARCHANDENT +MARCHANDIEZ +MARCHANTIE +MARCHATES +MARCHERAIENT +MARCHERS +MARCHIONS +MARCOTTA +MARCOTTASSE +MARCOTTENT +MARCOTTERIEZ +MARDI +MARECHALATS +MAREGRAMME +MAREMOTEUR +MAREYAGE +MARGARINE +MARGAUDAIENT +MARGAUDERAIT +MARGAUDIEZ +MARGEAIS +MARGEAT +MARGER +MARGERIONS +MARGINA +MARGINALISAI +MARGINAS +MARGINEE +MARGINERENT +MARGINONS +MARGOTANT +MARGOTENT +MARGOTERIEZ +MARGOTONS +MARGOTTASSE +MARGOTTERA +MARGOTTERONS +MARGOULETTE +MARGRAVIAT +MARIAI +MARIANISTES +MARIAUX +MARIERAIT +MARIEURS +MARIJUANA +MARINAI +MARINASSIEZ +MARINERAI +MARINERONT +MARINIERE +MARIOLE +MARITALES +MARIVAUDIEZ +MARKETAIT +MARKETATES +MARKETEUSES +MARKETTERAI +MARKETTES +MARLIS +MARMITAGE +MARMITASSENT +MARMITER +MARMITERIONS +MARMONNAI +MARMONNER +MARMOREENNES +MARMORISASSE +MARMORISEE +MARMORISONS +MARMOTTAMES +MARMOTTE +MARMOTTERAIS +MARMOTTEUR +MARMOUSET +MARNAIT +MARNATES +MARNERAIT +MARNEURS +MAROCAIN +MARONITE +MARONNASSE +MARONNERA +MARONNERONS +MAROQUINAGES +MAROQUINERA +MAROQUINIEZ +MAROUFLA +MAROUFLASSE +MAROUFLENT +MAROUFLERIEZ +MAROUTES +MARQUANTE +MARQUATES +MARQUERAIT +MARQUESANE +MARQUETAS +MARQUETEES +MARQUETEZ +MARQUEUSE +MARQUISIEN +MARRAINE +MARRANES +MARRASSIONS +MARRERAIENT +MARRES +MARRONNAGE +MARRONNERAI +MARRONNERONT +MARRUBES +MARSEILLAIS +MARSOUIN +MARSOUINAS +MARSOUINER +MARSUPIALE +MARTEL +MARTELAS +MARTELEES +MARTELERAS +MARTELEUR +MARTIENNES +MARTYRISA +MARTYRISERA +MARTYROLOGES +MARXISAIT +MARXISASSIEZ +MARXISER +MARXISERIONS +MARXISTE +MASAIS +MASCARONS +MASCULIN +MASCULINISAS +MASCULINISER +MASCULINS +MASKOUTAIN +MASQUA +MASQUANTES +MASQUE +MASQUERAS +MASQUIONS +MASSACRANTE +MASSACRATES +MASSACRERAIT +MASSACREURS +MASSAIENT +MASSASSES +MASSENT +MASSERENT +MASSETTES +MASSICOTAI +MASSICOTERAI +MASSIERE +MASSIFIAMES +MASSIFIERAIS +MASSIFIEZ +MASSIVES +MASSORETE +MASTABAS +MASTEGUAIS +MASTEGUAT +MASTEGUERAIS +MASTEGUEZ +MASTICAGES +MASTIFF +MASTIQUANT +MASTIQUEE +MASTIQUERENT +MASTIQUONS +MASTOIDES +MASTOLOGUES +MASTURBAIS +MASTURBAT +MASTURBE +MASTURBERAS +MASTURBIONS +MATADOR +MATAMATA +MATASSIEZ +MATCHAMES +MATCHE +MATCHERAS +MATCHICHES +MATELAS +MATELASSAS +MATELASSEES +MATELASSEREZ +MATELASSIERS +MATELOTES +MATEREAU +MATERIEZ +MATERNANT +MATERNEE +MATERNERAI +MATERNERONT +MATERNISAMES +MATERNISE +MATERNISERAS +MATERNISIONS +MATES +MATHS +MATIERISMES +MATIFIAIT +MATIFIASSIEZ +MATIFIERAI +MATIFIERONT +MATINAI +MATINAS +MATINEE +MATINERENT +MATINEZ +MATIR +MATIRIONS +MATISSES +MATOIS +MATORRALS +MATRAQUAIENT +MATRAQUES +MATRIARCAL +MATRICAGES +MATRICASSE +MATRICENT +MATRICERIEZ +MATRICIELLES +MATRICULAIS +MATRICULAT +MATRICULEZ +MATRILOCALES +MATRONYME +MATURA +MATURASSE +MATUREE +MATURERENT +MATURITE +MAUDIMES +MAUDIRIEZ +MAUDISSENT +MAUGRABIN +MAUGREANT +MAUGREBINE +MAUGREES +MAURANDIES +MAURICIENS +MAURRASSIENS +MAUVAIS +MAUVIETTES +MAXILLIPEDES +MAXIMALISAI +MAXIMALISERA +MAXIMANT +MAXIMISAIS +MAXIMISAT +MAXIMISERAI +MAXIMISERONT +MAXWELLS +MAYENNAISE +MAYONNAISES +MAZAGRANS +MAZARINADE +MAZATES +MAZEE +MAZERENT +MAZETTES +MAZOUTA +MAZOUTASSE +MAZOUTENT +MAZOUTERIEZ +MAZURKA +MEANDRE +MEATS +MECANIQUE +MECANISAS +MECANISE +MECANISERAS +MECANISIONS +MECENAT +MECHAIT +MECHASSE +MECHENT +MECHERIEZ +MECHIONS +MECONDUIRA +MECONDUIRONS +MECONDUISEZ +MECONDUISIT +MECONIAUX +MECONNAITRA +MECONNUSSES +MECONTENTAIT +MECONTENTE +MECONTENTIEZ +MECREANCE +MEDAILLABLE +MEDAILLER +MEDAILLIONS +MEDERSA +MEDIAMATS +MEDIAT +MEDIATIONS +MEDIATISANT +MEDIATISIEZ +MEDIAUX +MEDICAMENTAS +MEDICAMENTES +MEDICASTRES +MEDICINIER +MEDIEVAL +MEDIMNES +MEDIOPALATAL +MEDIRAIT +MEDISAIENT +MEDISENT +MEDISSIONS +MEDITASSE +MEDITATIONS +MEDITERAIENT +MEDITERRANE +MEDITIONS +MEDLEYS +MEDULLAS +MEDUSAMES +MEDUSE +MEDUSERAS +MEDUSIONS +MEFIAIT +MEFIASSENT +MEFIER +MEFIERIONS +MEGA +MEGAJOULE +MEGALO +MEGALOMANE +MEGALOPTERES +MEGAPODE +MEGARON +MEGIRA +MEGIRONS +MEGISSAMES +MEGISSE +MEGISSERAS +MEGISSEZ +MEGOHMMETRES +MEGOTAMES +MEGOTE +MEGOTERAS +MEGOTEUSE +MEHAREE +MEILLEURES +MEJANAGE +MEJUGEAS +MEJUGENT +MEJUGEREZ +MEKNASSIE +MELAIENT +MELANCOLIE +MELANGEAI +MELANGERAI +MELANGERONT +MELANINES +MELAS +MELBA +MELEAGRINES +MELERAI +MELERONT +MELILOT +MELIPHAGES +MELISSES +MELLIFERES +MELLITES +MELODIQUE +MELOMANES +MELONGINE +MELOPEES +MELUSINE +MEMBRUE +MEMERAGE +MEMERASSENT +MEMERERAI +MEMERERONT +MEMORABLE +MEMORIAUX +MEMORISAIS +MEMORISAT +MEMORISERAI +MEMORISERONT +MENACA +MENACAS +MENACEES +MENACEREZ +MENADE +MENAGEAS +MENAGEMENT +MENAGERAS +MENAGERONT +MENAIS +MENASSE +MENCHEVIQUES +MENDIAI +MENDIASSE +MENDIEE +MENDIERENT +MENDIGOTAI +MENDIGOTERAI +MENDOIS +MENENT +MENERIEZ +MENEURS +MENINE +MENINGITIQUE +MENISCALES +MENOLOGE +MENOPOME +MENOTAXIE +MENOTTANT +MENOTTEE +MENOTTERENT +MENOTTONS +MENSONGES +MENSTRUES +MENSUALISENT +MENSUEL +MENTAIENT +MENTALISAIS +MENTALISAT +MENTALISERAI +MENTANT +MENTEUSES +MENTHOLES +MENTIONNES +MENTIRAIS +MENTISME +MENTONNET +MENTORE +MENUISA +MENUISASSE +MENUISENT +MENUISERIE +MENUISIERES +MEO +MEPRENAIENT +MEPRENDRAS +MEPRENIONS +MEPRISABLES +MEPRISAS +MEPRISEES +MEPRISEREZ +MEPRISSE +MERANTI +MERCERIE +MERCERISANT +MERCERISEE +MERCERISIEZ +MERCIERES +MERCUREUSES +MERCURIEN +MERDAIENT +MERDASSES +MERDERA +MERDERONS +MERDIONS +MERDOIERAS +MERDOUILLAT +MERDOUILLONS +MERDOYASSENT +MERDOYIEZ +MERGUEZ +MERIDIONALE +MERINGUAIS +MERINGUAT +MERINGUERAIS +MERINGUEZ +MERISIERS +MERITAI +MERITASSE +MERITENT +MERITERIEZ +MERLEAU +MERLONNAIENT +MERLONNES +MERLUCHE +MERVEILLE +MESA +MESALLIAIS +MESALLIERAI +MESALLIERONT +MESANGETTE +MESCALS +MESENCHYME +MESESTIMERA +MESMERIENNES +MESOCOLON +MESOLOGIE +MESOPAUSE +MESOSPHERE +MESOTHORAX +MESQUINERIE +MESSAGERIES +MESSEIGNEURS +MESSIANISMES +MESSIEENT +MESSINE +MESTRANCES +MESURAIS +MESURAT +MESURERAIENT +MESURES +MESURONS +MESUSAS +MESUSER +MESUSERIONS +METABOLE +METABOLISANT +METABOLISEES +METABOLISONS +METACENTRES +METAGALAXIES +METALDEHYDE +METALLIFERE +METALLISANT +METALLISEURS +METALLOGENIE +METAPHORISEE +METAPHRASES +METAPLASIE +METASTASES +METAUX +METEILS +METEORIQUE +METEORISASSE +METEORISEE +METEORISME +METHANIERES +METHANISANT +METHANISIEZ +METHANOLS +METHODISME +METHYLAMINES +METICALS +METISSA +METISSASSE +METISSENT +METISSERIEZ +METONIENS +METRAGE +METRASSENT +METRER +METRERIONS +METRICIENNE +METRISATIONS +METROLOGUE +METRONS +METRORRAGIE +METTABLE +METTEURS +METTRAIENT +MEUBLA +MEUBLAS +MEUBLEES +MEUBLEREZ +MEUF +MEUGLASSE +MEUGLENT +MEUGLERIEZ +MEULA +MEULARDE +MEULATES +MEULERAIT +MEULES +MEULIERES +MEUNIERES +MEURSAULT +MEURTRIERE +MEURTRIRAIT +MEURTRISSAIS +MEURTRISSURE +MEUTE +MEVENDE +MEVENDISSENT +MEVENDRAIS +MEVENDU +MEZAIL +MEZOUZA +MIAM +MIAULAIS +MIAULAT +MIAULERAIS +MIAULEUR +MICACEE +MICHETONNA +MICHETONNIEZ +MICONIA +MICROBICIDES +MICROCEBE +MICROCOPIAIT +MICROCOPIE +MICROCOPIONS +MICROCREDITS +MICROFICHES +MICROFILMAT +MICROFILMIEZ +MICROFORME +MICROGRAPHIE +MICROLITIQUE +MICRONISAMES +MICRONISEZ +MICROPSIES +MICROSOCIETE +MICROSPORES +MICROTOMES +MICTIONNEL +MIDRASHIM +MIELLATS +MIELLURES +MIEVRE +MIGNARDA +MIGNARDASSES +MIGNARDERA +MIGNARDERONS +MIGNON +MIGNOTAI +MIGNOTASSIEZ +MIGNOTERAI +MIGNOTERONT +MIGRAINES +MIGRANTS +MIGRATEURS +MIGRERA +MIGRERONS +MIJAUREES +MIJOTAS +MIJOTEES +MIJOTEREZ +MIJOTIONS +MILANAISE +MILDIOUSEE +MILICES +MILITAIRE +MILITANTES +MILITARISANT +MILITARISE +MILITARISME +MILITASSIONS +MILITERAIT +MILITIEZ +MILLADES +MILLENAIRE +MILLEPERTUIS +MILLERANDES +MILLESIMASSE +MILLESIMENT +MILLETS +MILLIARDS +MILLIERS +MILLIMETRAIS +MIMAI +MIMASSIEZ +MIMERAI +MIMERONT +MIMINE +MIMOGRAPHES +MIMOSEE +MINAGES +MINARETS +MINAUDAI +MINAUDASSIEZ +MINAUDERAIS +MINAUDERONT +MINBAR +MINCIR +MINCIRIONS +MINCISSES +MINCOLETTES +MINERAIENT +MINERALISAT +MINERALISENT +MINERALOGIE +MINERENT +MINERVISTE +MINEUR +MINIATURE +MINIBARS +MINIDOSE +MINIJUPES +MINIMAUX +MINIMISA +MINIMISASSES +MINIMISENT +MINIMISERIEZ +MINIMUMS +MINISTERES +MINISTRES +MINIVAGUES +MINOENS +MINORANT +MINORAT +MINOREE +MINORERENT +MINORISA +MINORISASSES +MINORISERA +MINORISERONS +MINORITES +MINOTES +MINOUCHAIS +MINOUCHAT +MINOUCHERAIS +MINOUCHEZ +MINSKOIS +MINUTAGES +MINUTASSE +MINUTENT +MINUTERIE +MINUTIE +MINUTONS +MIRABELLE +MIRACULEE +MIRAGES +MIRASSES +MIRE +MIRERAIT +MIRETTES +MIRIONS +MIRMILLONS +MIROITA +MIROITAS +MIROITEES +MIROITERAS +MIROITEZ +MIRONTONS +MISAINES +MISANTHROPES +MISASSIONS +MISEES +MISERABLES +MISERES +MISERIONS +MISOGAMIE +MISONS +MISSIEZ +MISSIONNES +MISTER +MISTRAL +MITAINES +MITASSE +MITENT +MITERIEZ +MITHRACISME +MITIGEAIENT +MITIGERAIENT +MITIGES +MITONNAIT +MITONNATES +MITONNERAIT +MITONNIEZ +MITOYENNES +MITRAILLES +MITRAILLONS +MITRONS +MIXAIS +MIXAT +MIXERAIS +MIXES +MIXONS +MIXTIONNES +MNEMES +MOABITE +MOBILE +MOBILISABLES +MOBILISASSES +MOBILISERAIT +MOBILISIEZ +MOBILOPHONES +MOCHARD +MODAL +MODALISAS +MODALISE +MODALISERAS +MODALISIONS +MODELAGES +MODELASSES +MODELERA +MODELERONS +MODELISA +MODELISASSES +MODELISERAIT +MODELISIEZ +MODENAIS +MODERAIS +MODERASSENT +MODERATO +MODERERA +MODERERONS +MODERNISEE +MODERNISME +MODESTEMENT +MODIFIA +MODIFIANTES +MODIFICATEUR +MODIFIE +MODIFIERAS +MODIFIIONS +MODULA +MODULANT +MODULASSIEZ +MODULE +MODULERAS +MODULIONS +MOELLEUX +MOFETTE +MOFFLASSE +MOFFLENT +MOFFLERIEZ +MOFLAI +MOFLASSIEZ +MOFLERAI +MOFLERONT +MOGHOLES +MOHICAN +MOIERAI +MOIES +MOINERIES +MOIRAIS +MOIRAT +MOIRERAIS +MOIREUR +MOIS +MOISAS +MOISEES +MOISEREZ +MOISIEZ +MOISIRENT +MOISISSANT +MOISIT +MOISSONNAGES +MOISSONNERA +MOISSONNONS +MOITIR +MOITIRIONS +MOITISSES +MOJITOS +MOLARD +MOLARDASSENT +MOLARDERAI +MOLARDERONT +MOLASSES +MOLECULE +MOLESTAIS +MOLESTAT +MOLESTERAIS +MOLESTEZ +MOLETAIT +MOLETATES +MOLETOIR +MOLETTERAS +MOLIERESQUES +MOLLACHUE +MOLLARDAIENT +MOLLARDERAIT +MOLLARDIEZ +MOLLASSON +MOLLETIERE +MOLLETONNAS +MOLLETONNENT +MOLLETONNONS +MOLLIRA +MOLLIRONS +MOLLISSENT +MOLOSSOIDES +MOLYBDENES +MOME +MOMES +MOMIFIAIS +MOMIFIAT +MOMIFIERAI +MOMIFIERONT +MOMINETTE +MONACHISMES +MONADOLOGIES +MONARCHISAI +MONARCHISEZ +MONARQUE +MONAZITE +MONDA +MONDANITE +MONDATES +MONDERAIT +MONDEUSES +MONDIALISAIT +MONDIALISIEZ +MONDIEZ +MONEMES +MONETISAIS +MONETISAT +MONETISERAI +MONETISERONT +MONGOLIENNE +MONIAL +MONILIFORME +MONITION +MONITORAIS +MONITORAT +MONITORES +MONNAIE +MONNAIERIONS +MONNAYAIENT +MONNAYES +MONOAMINE +MONOBLOC +MONOCAMERAUX +MONOCEPAGES +MONOCLE +MONOCLONALES +MONOCORPS +MONOCYLINDRE +MONODOSES +MONOGATARIS +MONOGRADES +MONOIDE +MONOKINIS +MONOLOGUERAI +MONOMANIAQUE +MONONUCLEE +MONOPLACES +MONOPOLEUSES +MONOPOLISE +MONOPOLISONS +MONORCHIDIE +MONOSEMIES +MONOSKIS +MONOSTABLES +MONOSYLLABES +MONOTHEISMES +MONOTONES +MONOTYPES +MONOXYLES +MONSIGNOR +MONSTRATIONS +MONSTRUEUX +MONTAGNAISE +MONTAGNEUSE +MONTANTS +MONTBELIARDE +MONTENT +MONTEURS +MONTICOLE +MONTJOIE +MONTRA +MONTRANT +MONTREALAIS +MONTRERAIS +MONTREUSIENS +MONTURES +MOORES +MOQUASSENT +MOQUER +MOQUERIES +MOQUETTAIT +MOQUETTATES +MOQUETTERAIT +MOQUETTIEZ +MOQUEZ +MORAILLAIS +MORAILLAT +MORAILLERAIS +MORAILLEZ +MORALE +MORALISANTE +MORALISATES +MORALISENT +MORALISERIEZ +MORALISIEZ +MORASSES +MORBIDE +MORCELABLE +MORCELASSENT +MORCELERENT +MORCELLERAI +MORCELLES +MORDAIT +MORDANCAS +MORDANCEES +MORDANCEREZ +MORDANT +MORDES +MORDICUS +MORDILLAIT +MORDILLATES +MORDILLES +MORDISSENT +MORDORAIS +MORDORAT +MORDORERAIS +MORDOREZ +MORDRAIT +MORDUE +MORENE +MORFALAIT +MORFALATES +MORFALERAIT +MORFALIEZ +MORFILAIS +MORFILAT +MORFILERAIS +MORFILEZ +MORFLAMES +MORFLE +MORFLERAS +MORFLIONS +MORFONDEZ +MORFONDRE +MORFONDUS +MORGUAI +MORGUASSIEZ +MORGUERA +MORGUERONS +MORIBONDES +MORIGENAIT +MORIGENATES +MORIGENERAIT +MORIGENIEZ +MORION +MORMONS +MORNIFLES +MORPHEME +MORPHINOMANE +MORSURES +MORTAISAIT +MORTAISATES +MORTAISERAIT +MORTAISEURS +MORTEAU +MORTIFERE +MORTIFIANTES +MORTIFIERAIS +MORTIFIEZ +MORUES +MORVANDELLES +MORVEUSES +MOSAIQUAMES +MOSAIQUE +MOSAIQUERAS +MOSAIQUIONS +MOSCOUTAIRE +MOSETTE +MOSSIES +MOTARDS +MOTIFS +MOTIONNAIT +MOTIONNATES +MOTIONNERENT +MOTIONNONS +MOTIVANTES +MOTIVATION +MOTIVERA +MOTIVERONS +MOTOBINEUSE +MOTOCULTURES +MOTONEIGES +MOTOPAVEURS +MOTORISAI +MOTORISER +MOTORSHIP +MOTRICES +MOTTANT +MOTTEE +MOTTERENT +MOTTIONS +MOUCHAIENT +MOUCHARDA +MOUCHARDASSE +MOUCHARDENT +MOUCHAS +MOUCHEES +MOUCHEREZ +MOUCHERONNER +MOUCHETA +MOUCHETASSES +MOUCHETES +MOUCHETTERAI +MOUCHETTES +MOUCHOIR +MOUDJAHIDIN +MOUDREZ +MOUFETA +MOUFETASSES +MOUFETES +MOUFLAGE +MOUFLASSENT +MOUFLER +MOUFLERIONS +MOUFLIONS +MOUFTAS +MOUFTER +MOUFTERIONS +MOUILLAIT +MOUILLASSAIT +MOUILLAT +MOUILLERAI +MOUILLERIONS +MOUILLEZ +MOUISES +MOULAGE +MOULANTS +MOULEE +MOULERENT +MOULEUSES +MOULINAIENT +MOULINES +MOULINIER +MOULINS +MOULUE +MOULURAMES +MOULURATION +MOULURERAIS +MOULUREZ +MOULUSSENT +MOUNDAS +MOURANTS +MOUROIRS +MOURRES +MOURUSSENT +MOUSCRONNOIS +MOUSQUETERIE +MOUSSAILLON +MOUSSANTS +MOUSSEAU +MOUSSERAIENT +MOUSSERONT +MOUSSON +MOUSTACHES +MOUSTIER +MOUTARDAIENT +MOUTARDERA +MOUTARDERONS +MOUTIER +MOUTONNAMES +MOUTONNERA +MOUTONNERIEZ +MOUTONNIERE +MOUVAIENT +MOUVANTS +MOUVEMENTER +MOUVEZ +MOUVRAS +MOXAS +MOYAS +MOYEES +MOYENNAMES +MOYENNE +MOYENNERAIT +MOYENNIEZ +MOYEUX +MOZAMBICAINS +MOZZARELLAS +MUANCE +MUATES +MUCHASSENT +MUCHER +MUCHERIONS +MUCORACEE +MUDEJARES +MUERAI +MUERONT +MUEZZINS +MUFTIS +MUGIR +MUGIRIONS +MUGISSANTS +MUGIT +MUGUETANT +MUGUETEE +MUGUETTE +MULARDES +MULATRE +MULET +MULHOUSIEN +MULLIDES +MULTIBRINS +MULTICOLORE +MULTIFORME +MULTILATERAL +MULTIMETRE +MULTINORME +MULTIPLE +MULTIPLEXAIS +MULTIPLEXEZ +MULTIPLIAIT +MULTIPLIATES +MULTIPLIERAI +MULTIPOLAIRE +MULTIVARIEE +MUMUSE +MUNICIPALE +MUNICIPAUX +MUNIFICENTS +MUNIREZ +MUNISSE +MUNITIONNA +MUNITIONNER +MUNTJAC +MUQUEUSES +MURAILLA +MURAILLASSES +MURAILLENT +MURAILLERIEZ +MURAIT +MURANT +MURE +MURERA +MURERIEZ +MURETTES +MURGEANT +MURGEES +MURGERENT +MURGIONS +MURIERS +MURIRA +MURIRONS +MURISSANTES +MURISSIEZ +MURMURAIS +MURMURASSES +MURMURERA +MURMURERONS +MURRHES +MUSAGETE +MUSARD +MUSARDASSENT +MUSARDERAI +MUSARDERIONS +MUSARDIONS +MUSASSIONS +MUSCADIER +MUSCARI +MUSCHELKALK +MUSCINALES +MUSCLANT +MUSCLEE +MUSCLERENT +MUSCLONS +MUSCULATURES +MUSEAUX +MUSEIFIAS +MUSEIFIEES +MUSEIFIEREZ +MUSELA +MUSELASSE +MUSELER +MUSELLE +MUSELLEREZ +MUSEOGRAPHIE +MUSER +MUSERIONS +MUSEZ +MUSICASSETTE +MUSIQUAS +MUSIQUEES +MUSIQUEREZ +MUSIQUIONS +MUSQUAIS +MUSQUAT +MUSQUERAIS +MUSQUEZ +MUSSANT +MUSSEE +MUSSERENT +MUSSIFS +MUSSOLINIEN +MUSTS +MUTABLES +MUTAIT +MUTASSIEZ +MUTATIONNELS +MUTE +MUTERAS +MUTILA +MUTILAS +MUTILATION +MUTILERAI +MUTILERONT +MUTINAIS +MUTINAT +MUTINERAIS +MUTINERONT +MUTISME +MUTUALISAMES +MUTUALISEZ +MUTUEL +MUTULES +MYATONIES +MYCENIENNE +MYCOPLASMES +MYCORHIZIENS +MYDRIATIQUE +MYELINISEE +MYELOGRAMMES +MYELOPATHIES +MYLONITE +MYOCARDIQUES +MYOGLOBINES +MYOLOGIQUES +MYOPATHES +MYOPOTAMES +MYOSITE +MYRIAMETRES +MYRMIDONS +MYROXYLONS +MYRTIFORMES +MYSTAGOGUES +MYSTICISMES +MYSTIFIAMES +MYSTIFIEE +MYSTIFIERENT +MYSTIFIONS +MYTHIFIAIT +MYTHIFIATES +MYTHIFIES +MYTHOGRAPHIE +MYXOVIRUS +NABATEENNES +NACRAI +NACRASSIEZ +NACRERAI +NACRERONT +NADIRAL +NAGAIKA +NAGEAIT +NAGEATES +NAGEOTAIS +NAGEOTAT +NAGEOTERAS +NAGEOTIONS +NAGEREZ +NAGEZ +NAHUAS +NAIN +NAIS +NAISSANT +NAISSIEZ +NAITRE +NAIVETES +NAMUROISE +NANCEIENNE +NANIFIAI +NANIFIASSIEZ +NANIFIERAI +NANIFIERONT +NANISAIT +NANISATES +NANISERAIT +NANISIEZ +NANS +NANTERROIS +NANTIRAIENT +NANTIS +NANTISSEZ +NAOS +NAPHTALINE +NAPHTOLS +NAPOLITAINS +NAPPAS +NAPPEES +NAPPEREZ +NAPPONS +NAQUITES +NARCISSIQUES +NARCOTINES +NARGHILE +NARGUANT +NARGUEE +NARGUERENT +NARGUILES +NARRAI +NARRASSIEZ +NARRATION +NARRATRICE +NARRERAIS +NARREZ +NASALE +NASALISASSE +NASALISEE +NASALISERENT +NASALISONS +NASE +NASILLAIT +NASILLASSES +NASILLENT +NASILLERIEZ +NASILLIEZ +NASKAPIS +NASONNASSENT +NASONNEMENTS +NASONNEREZ +NASSE +NATALITES +NATIFS +NATIONALISME +NATIVEMENT +NATOUFIENNES +NATRUM +NATTANT +NATTEE +NATTERENT +NATTIERES +NATURELLE +NATUROPATHES +NAUFRAGES +NAUPATHIE +NAUSEABONDE +NAUTIQUE +NAVAJO +NAVARQUE +NAVEL +NAVETTAMES +NAVETTE +NAVETTEREZ +NAVETTEZ +NAVICULE +NAVIGATEUR +NAVIGUAIT +NAVIGUATES +NAVIGUERENT +NAVIGUONS +NAVRAIS +NAVRASSES +NAVRENT +NAVRERIEZ +NAYS +NAZCAS +NAZIFIAIT +NAZIFIATES +NAZIFIES +NAZISME +NEANTISAIENT +NEANTISERA +NEANTISERONS +NEBKHA +NEBULISAIENT +NEBULISERA +NEBULISERONS +NEBULOSITES +NECESSITANT +NECESSITAT +NECESSITEUSE +NECROBIE +NECROLOGUE +NECROMANTES +NECROPHOBE +NECROS +NECROSASSENT +NECROSER +NECROSERIONS +NECROTIQUE +NEEMS +NEFLE +NEGATIVITE +NEGLIGEABLE +NEGLIGENCES +NEGLIGERAIS +NEGLIGEZ +NEGOCIAI +NEGOCIASSE +NEGOCIATIONS +NEGOCIES +NEGRIERS +NEIGE +NEIGEOTERAIT +NELOMBO +NEMATOCYSTES +NEMERTIENS +NENE +NEOBLASTE +NEOCLASSIQUE +NEODARWINIEN +NEOFASCISTE +NEOGRECQUES +NEOLIBERALES +NEOLOCALES +NEOLOGISAIT +NEOLOGISATES +NEOLOGISME +NEONATAL +NEONAZIE +NEOPILINA +NEOPLASTIE +NEOPRENE +NEORURAL +NEOTENIES +NEPE +NEPETAS +NEPHELIONS +NEPHRITES +NEPHROLOGUES +NEPTUNISMES +NERF +NERONIENNES +NERVEUSE +NERVOSISME +NERVURAIT +NERVURATES +NERVURERAIT +NERVURIEZ +NESTORIENNES +NETSUKE +NETTOIENT +NETTOIERONS +NETTOYAIS +NETTOYASSES +NETTOYES +NETWORKS +NEUMATIQUE +NEUROGENE +NEUROLOGISTE +NEUROPATHIES +NEUROTOMIES +NEUSTON +NEUTRALISENT +NEUTRALISTE +NEUTRON +NEUVAINE +NEVERSOISE +NEVRALGIES +NEVRITIQUES +NEVROPATHIES +NEVROSIQUES +NEWTON +NGULTRUMS +NIAISA +NIAISASSE +NIAISEMENT +NIAISEREZ +NIAISEUX +NIAMEYENS +NIASSENT +NIBARD +NICHAI +NICHASSIEZ +NICHERAI +NICHERONT +NICHIONS +NICKELAGES +NICKELASSES +NICKELES +NICKELLERA +NICKELLERONT +NICOIS +NICOSIENNE +NICOTINISMES +NICTITATIONS +NIDIFIA +NIDIFIASSES +NIDIFIERENT +NIDIFIONS +NIDWALDIENS +NIELLAGES +NIELLASSES +NIELLERA +NIELLERONS +NIELLONS +NIERAI +NIERONT +NIFES +NIGERIAN +NIHILISME +NILLE +NIMBAI +NIMBASSIEZ +NIMBERAI +NIMBERONT +NIMOISES +NIOLO +NIPPA +NIPPASSES +NIPPERA +NIPPERONS +NIPPONNES +NIQUANT +NIQUEDOUILLE +NIQUERAIT +NIQUIEZ +NITOUCHE +NITRANTES +NITRATAI +NITRATASSIEZ +NITRATER +NITRATERIONS +NITREE +NITRERENT +NITREZ +NITRIFIANT +NITRIFIAT +NITRIFIEES +NITRIFIEREZ +NITRILE +NITROGENE +NITROSAIENT +NITROSASSES +NITROSENT +NITROSERIEZ +NITROSYLE +NITRURANT +NITRURATIONS +NITRURERAIT +NITRURIEZ +NIVATIONS +NIVELAGES +NIVELASSES +NIVELES +NIVELLE +NIVELLEREZ +NIVEOLE +NIZERE +NOBELISAIT +NOBELISATES +NOBELISERAIT +NOBELISIEZ +NOBLAILLONNE +NOCA +NOCASSES +NOCERAIENT +NOCES +NOCICEPTEUR +NOCIFS +NOCTULES +NODAUX +NOEL +NOEUD +NOIEREZ +NOIRAUDE +NOIRCIRA +NOIRCIRONS +NOIRCISSENT +NOIRCISSURE +NOISES +NOLISAI +NOLISASSIEZ +NOLISER +NOLISERIONS +NOM +NOMADISANT +NOMADISERAIT +NOMADISIEZ +NOMBRAIENT +NOMBRASSIONS +NOMBRERAIENT +NOMBRES +NOMBRILISTES +NOMINAL +NOMINALISANT +NOMINALISE +NOMINALISME +NOMINASSES +NOMINERAIENT +NOMINES +NOMMAI +NOMMASSIEZ +NOMMERA +NOMMERONS +NOMOGRAPHES +NON +NONCE +NONCHALOIRS +NONNETTES +NONUPLAI +NONUPLASSIEZ +NONUPLERAI +NONUPLERONT +NOPAL +NORDET +NORDIRAI +NORDIRONT +NORDISSIEZ +NORIEN +NORMAL +NORMALISEE +NORMALISONS +NORMASSE +NORMATIVES +NORMEES +NORMEREZ +NORMOGRAPHES +NORVEGIEN +NOSOCOMIAUX +NOSOLOGIQUES +NOSTOCS +NOTAIRE +NOTARIALE +NOTASSE +NOTATIONS +NOTERA +NOTERONS +NOTIFIAIENT +NOTIFIEE +NOTIFIERENT +NOTIFIONS +NOTOIREMENT +NOTULES +NOUASSIONS +NOUCS +NOUERAIENT +NOUES +NOUGATINE +NOULETS +NOUMENES +NOURRICERIES +NOURRIRA +NOURRIRONS +NOURRISSIEZ +NOUURES +NOUVELLISTES +NOVASSE +NOVATIONS +NOVELISA +NOVELISASSES +NOVELISENT +NOVELISERIEZ +NOVELLES +NOVELLISEES +NOVELLISEREZ +NOVEMBRE +NOVERENT +NOVICES +NOVIONS +NOYA +NOYANT +NOYAUTA +NOYAUTASSE +NOYAUTENT +NOYAUTERIEZ +NOYAUTIEZ +NOYERENT +NUAGES +NUAMES +NUANCASSENT +NUANCEMENTS +NUANCEREZ +NUANCIONS +NUATES +NUBILITES +NUCELLES +NUCLEARISAS +NUCLEARISEE +NUCLEARISTE +NUCLEINE +NUCLEOLYSE +NUDISMES +NUER +NUERIONS +NUIRA +NUIRONS +NUISENT +NUISIEZ +NUISIT +NUITEES +NULLE +NUMERAIRES +NUMERATIVE +NUMERISAIENT +NUMERISERA +NUMERISERONS +NUMEROLOGIE +NUMEROTAI +NUMEROTER +NUMEROTIONS +NUMMULITE +NUNCHAKU +NUONS +NURAGHES +NURSERYS +NUTRICIERS +NYCTHEMERAUX +NYMPHALIDE +NYMPHEES +NYMPHOSE +OASIS +OBEDIENCIERS +OBEIRA +OBEIRONS +OBEISSANTES +OBEITES +OBERAIS +OBERAT +OBERERAIS +OBEREZ +OBIERS +OBJECTAIT +OBJECTASSIEZ +OBJECTERA +OBJECTERONS +OBJECTIONS +OBJECTIVAS +OBJECTIVE +OBJECTIVIEZ +OBJECTRICE +OBLATES +OBLATURE +OBLIGEA +OBLIGEANTE +OBLIGEATES +OBLIGERAIT +OBLIGIEZ +OBLIQUASSE +OBLIQUER +OBLIQUERIONS +OBLITERA +OBLITERASSES +OBLITERERAIT +OBLITERIEZ +OBNUBILAIS +OBNUBILAT +OBNUBILERAI +OBNUBILERONT +OBOMBRAIENT +OBOMBRES +OBSCURCIR +OBSCURES +OBSEDAIT +OBSEDASSIEZ +OBSEDERAI +OBSEDERONT +OBSEQUES +OBSERVABLE +OBSERVANTIN +OBSERVAT +OBSERVATRICE +OBSERVERAIS +OBSERVEZ +OBSOLESCENCE +OBSTETRICAL +OBSTINA +OBSTINASSES +OBSTINEMENT +OBSTINEREZ +OBSTINEUX +OBSTRUAMES +OBSTRUCTIF +OBSTRUER +OBSTRUERIONS +OBTEMPERIEZ +OBTENIR +OBTENUES +OBTIENDRIONS +OBTINS +OBTURAIENT +OBTURASSIONS +OBTUREE +OBTURERENT +OBTURONS +OBUSIERS +OBVENU +OBVIAIS +OBVIAT +OBVIENDRIEZ +OBVIERA +OBVIERONS +OBVINSSENT +OBWALDIENS +OCCASIONNAIS +OCCASIONNAT +OCCASIONNER +OCCIDENTALE +OCCIPUTS +OCCITANISTE +OCCLUEZ +OCCLURAS +OCCLUSALE +OCCLUSIVES +OCCULTAIENT +OCCULTERA +OCCULTERONS +OCCULTONS +OCCUPANTS +OCCUPERAI +OCCUPERONT +OCCURRENTES +OCEANIENNE +OCELLEE +OCHRONOSES +OCRASSENT +OCRER +OCRERIONS +OCRONS +OCTANOIQUE +OCTAVE +OCTAVIASSE +OCTAVIENT +OCTAVIERIEZ +OCTAVIONS +OCTIDI +OCTOGONALE +OCTOSYLLABES +OCTROIERAIT +OCTROYAI +OCTROYASSIEZ +OCTROYEZ +OCTUPLAIT +OCTUPLATES +OCTUPLERAIT +OCTUPLIEZ +OCULES +OCULUS +ODEON +ODOLOGIES +ODORIFERANTE +ODORISANTS +ODORISATIONS +ODORISERAIT +ODORISEURS +OECUMENIQUES +OEDEMATIEES +OEDIPIENNES +OEILLETON +OEILLETONNAS +OEILLETONNES +OEKOUMENE +OENOLISME +OENOMETRIE +OENOTHERA +OESOPHAGE +OESTRES +OESTRONES +OEUVRA +OEUVRASSES +OEUVRERAIENT +OEUVRES +OFFENSAIENT +OFFENSASSENT +OFFENSER +OFFENSERIONS +OFFENSIONS +OFFERTS +OFFICIALISAI +OFFICIASSE +OFFICIELLE +OFFICIERAIT +OFFICIERS +OFFICINALE +OFFRANDES +OFFREZ +OFFRIRAS +OFFRISSES +OFFSHORE +OFFUSQUASSE +OFFUSQUENT +OFFUSQUERIEZ +OFLAGS +OGIVALE +OGRESSES +OIDIES +OIGNENT +OIGNISSES +OIGNONS +OINDRE +OINTE +OISELAMES +OISELE +OISELEUSE +OISELLENT +OISELLERIEZ +OISIFS +OJIBWA +OKRA +OLECRANIEN +OLEICOLE +OLEIFIANT +OLENEKIEN +OLFACTION +OLIBRIUS +OLIGISTES +OLIVACEE +OLIVAISONS +OLIVETAIN +OLIVEUSES +OLOGRAPHE +OLYMPIENNES +OMBELLALE +OMBELLIFORME +OMBILIQUE +OMBRAGEAIENT +OMBRAGES +OMBRAIS +OMBRAT +OMBRERAI +OMBRERONT +OMBRIENNES +OMBUDSMANS +OMETTAIT +OMETTRAI +OMETTRONT +OMISSENT +OMNICOLORE +OMNIPOTENT +OMNIPRESENTE +OMNIUM +ONAGRACEES +ONANISMES +ONCIALES +ONCOLOGIE +ONCQUES +ONDATRAS +ONDINE +ONDOIERAI +ONDOIES +ONDOYANTS +ONDOYEE +ONDULAI +ONDULASSE +ONDULATOIRES +ONDULERAIT +ONDULEURS +ONEREUSES +ONGLIER +ONGUIFORME +ONIRISME +ONLINE +ONTOGENESES +ONYCHOMYCOSE +ONZAIN +OOGENESE +OOLITIQUE +OPACIFIA +OPACIFIAS +OPACIFIE +OPACIFIERAS +OPACIFIIONS +OPALESCENCE +OPALISA +OPALISASSES +OPALISENT +OPALISERIEZ +OPAQUES +OPERABLES +OPERANTES +OPERATEUR +OPERATOIRE +OPERE +OPERERAS +OPEREZ +OPHIDIENNES +OPHIOLITIQUE +OPHIURIDE +OPIACAS +OPIACEES +OPIACEREZ +OPIAT +OPINAMES +OPINE +OPINERAS +OPINIATRAI +OPINIATRERA +OPINION +OPODELDOCH +OPOTHERAPIE +OPPORTUNES +OPPOSANTES +OPPOSE +OPPOSERAS +OPPOSIONS +OPPRESSAI +OPPRESSASSE +OPPRESSENT +OPPRESSERIEZ +OPPRESSIFS +OPPRIMAIT +OPPRIMASSIEZ +OPPRIMERAI +OPPRIMERONT +OPSOMANE +OPTAIENT +OPTASSIONS +OPTERA +OPTERONS +OPTIMAL +OPTIMALISAS +OPTIMALISEE +OPTIMAUX +OPTIMISEES +OPTIMISEREZ +OPTIMISMES +OPTIONNELS +OPTOMETRISTE +OPULENCES +OPUSCULE +ORAGEUX +ORALISAIS +ORALISAT +ORALISERAIS +ORALISEZ +ORANGEA +ORANGEASSE +ORANGENT +ORANGERAS +ORANGES +ORANT +ORATORIO +ORBICULAIRES +ORBITALES +ORBITATES +ORBITERAIS +ORBITEUR +ORCANETTE +ORCHESTRAIT +ORCHESTRAUX +ORCHESTRIEZ +ORCHIDEES +ORDALIQUE +ORDINANDS +ORDONNA +ORDONNANCAT +ORDONNANCES +ORDONNASSENT +ORDONNE +ORDONNERAS +ORDONNIONS +ORDURE +OREES +OREILLETTES +ORFEVRERIE +ORGANEAU +ORGANICISMES +ORGANISANT +ORGANISER +ORGANISMES +ORGANOGENESE +ORGANSINES +ORGANZA +ORGEATS +ORGIE +ORICHALQUE +ORIENTAIS +ORIENTATES +ORIENTER +ORIENTERIONS +ORIENTIONS +ORIGANS +ORIGINALE +ORIGINASSES +ORIGINELLE +ORIGINERAIT +ORIGINIEZ +ORIOLES +ORLEANAISES +ORME +ORMOIE +ORNANT +ORNEE +ORNEMENTAL +ORNEMENTER +ORNER +ORNERIONS +ORNIONS +ORNITHOSES +OROGENESE +OROMETRIE +OROPHARYNX +ORPHELINAT +ORPHEONS +ORPINS +ORTHOCENTRE +ORTHODOXE +ORTHOGENESE +ORTHONORMES +ORTHOPNEES +ORTHOPTISTES +ORTOLANS +ORWELLIENS +OSAIS +OSAT +OSCARISAMES +OSCARISE +OSCARISERAS +OSCARISIONS +OSCILLAIS +OSCILLASSES +OSCILLERENT +OSCINES +OSEE +OSERAIES +OSES +OSIRIEN +OSMIQUE +OSMOSE +OSSEINES +OSSIANIQUES +OSSIFIAIS +OSSIFIAT +OSSIFIERAI +OSSIFIERONT +OSSUES +OSTEITES +OSTENSIONS +OSTEOCLASIES +OSTEOGENIES +OSTEOPATHIES +OSTEOPOROSES +OSTOS +OSTRACISAIT +OSTRACISATES +OSTRACISIEZ +OSTRAKON +OSTREIDE +OSTROGOTHS +OTAI +OTARIE +OTE +OTERAS +OTHELLOS +OTOCYONS +OTOLOGIQUES +OTOSCLEROSE +OTTAVIENNES +OU +OUAIS +OUATAGE +OUATASSENT +OUATER +OUATERIES +OUATIEZ +OUATINASSENT +OUATINER +OUATINERIONS +OUBLI +OUBLIAS +OUBLIEES +OUBLIEREZ +OUBLIEUX +OUCHES +OUF +OUGRIENS +OUIGHOURES +OUILLAIENT +OUILLASSIONS +OUILLERAIENT +OUILLERONS +OUIR +OULIPIENNES +OUOLOF +OURAQUES +OURDIRAIT +OURDISSAGES +OURDISSEUSE +OURDOU +OURLAIT +OURLATES +OURLERAIT +OURLETS +OURSE +OUST +OUTARDE +OUTILLAI +OUTILLASSIEZ +OUTILLERAI +OUTILLERONT +OUTILS +OUTRAGE +OUTRAGEANTS +OUTRAGEES +OUTRAGERENT +OUTRAGEUX +OUTRANCES +OUTRASSIEZ +OUTREPASSAIT +OUTREPASSE +OUTREPASSONS +OUTRERIEZ +OUTRONS +OUVERTURE +OUVRAGEAIENT +OUVRAGES +OUVRAMES +OUVRASSIONS +OUVRERA +OUVRERONS +OUVRIERES +OUVRIRA +OUVRIRONS +OUVROIR +OVAIRES +OVALISAI +OVALISASSIEZ +OVALISER +OVALISERIONS +OVARIEN +OVATIONNAI +OVATIONNERAI +OVERDOSE +OVICULE +OVINES +OVNI +OVOIDAL +OVOVIVIPARES +OVULAMES +OVULATION +OVULERAIS +OVULEZ +OXALIDE +OXFORDIEN +OXIMES +OXYCOUPAGES +OXYDAI +OXYDASE +OXYDATIFS +OXYDERA +OXYDERONS +OXYDONS +OXYGENABLES +OXYGENAS +OXYGENATEUR +OXYGENERAI +OXYGENERONT +OXYLITHES +OXYSULFURES +OYAT +OZENEUSES +OZONAIT +OZONATES +OZONERA +OZONERONS +OZONISA +OZONISASSES +OZONISEE +OZONISERENT +OZONISIEZ +PACAGEA +PACAGEASSES +PACAGERA +PACAGERONS +PACAS +PACHALIK +PACHTOUNES +PACHYURES +PACIFIANTS +PACIFIERA +PACIFIERONS +PACIFISME +PACKAGEURS +PACQUAGES +PACQUASSES +PACQUERA +PACQUERONS +PACSAIENT +PACSASSIONS +PACSERAIENT +PACSES +PACTISAIENT +PACTISERAIT +PACTISIEZ +PADDINGS +PADINES +PADOUS +PAGAIENT +PAGAIERONS +PAGANISAIENT +PAGANISES +PAGAYAIS +PAGAYAT +PAGAYERAS +PAGAYEUSE +PAGEAIS +PAGEAT +PAGEOT +PAGEOTASSENT +PAGEOTER +PAGEOTERIONS +PAGERA +PAGERONS +PAGIEZ +PAGINASSENT +PAGINEES +PAGINEREZ +PAGINIONS +PAGNOTAIENT +PAGNOTES +PAGRE +PAICHES +PAIENT +PAIERIEZ +PAILLAIS +PAILLARDAMES +PAILLARDE +PAILLARDIEZ +PAILLASSIEZ +PAILLAT +PAILLERAIS +PAILLES +PAILLETANT +PAILLETEE +PAILLETIEZ +PAILLEUSE +PAILLONNES +PAIN +PAIRESSES +PAISIBLEMENT +PAISSANTS +PAISSONS +PAITRIEZ +PAL +PALABRASSE +PALABRERA +PALABRERONS +PALABRONS +PALAISIENNES +PALANGRES +PALANQUAMES +PALANQUE +PALANQUERAS +PALANQUIN +PALATALES +PALATALISENT +PALATIALE +PALATRES +PALEE +PALEOLOGUE +PALEOZOIQUE +PALES +PALETTISAI +PALETTISER +PALETUVIER +PALICHONS +PALIFIAI +PALIFIASSIEZ +PALIFIER +PALIFIERIONS +PALILALIE +PALIRAIT +PALISSADA +PALISSADERA +PALISSAGES +PALISSANTES +PALISSE +PALISSERAS +PALISSIONS +PALISSONNER +PALIT +PALLADIUM +PALLIAIENT +PALLIASSIONS +PALLICARES +PALLIERA +PALLIERONS +PALLIUMS +PALMAMES +PALMASSIONS +PALMERA +PALMERIEZ +PALMIERS +PALMIPARTIE +PALMISEQUES +PALMITIQUES +PALOMBIERES +PALOTAIENT +PALOTASSIONS +PALOTERAIENT +PALOTES +PALOURDE +PALPAIS +PALPAT +PALPEES +PALPEREZ +PALPIEZ +PALPITANTES +PALPITATION +PALPITERAS +PALPITIONS +PALUCHA +PALUCHASSES +PALUCHERA +PALUCHERONS +PALUDE +PALUDIERES +PALUDOLOGUE +PALYNOLOGUE +PAMASSE +PAMENT +PAMERIEZ +PAMONS +PAMPILLES +PANACEE +PANACHAMES +PANACHE +PANACHERAS +PANACHIONS +PANAMEEN +PANAMIEN +PANARDE +PANAT +PANATHENIENS +PANCANADIENS +PANCREAS +PANDAS +PANEGYRIQUE +PANELISTES +PANEREE +PANETERIES +PANIC +PANIER +PANIFIAIS +PANIFIAT +PANIFIERAI +PANIFIERONT +PANIQUAI +PANIQUARDE +PANIQUATES +PANIQUERAIT +PANIQUIEZ +PANJABIS +PANNEAUTAGE +PANNEAUTER +PANNEAUX +PANNICULE +PANOSSAI +PANOSSASSIEZ +PANOSSERAI +PANOSSERONT +PANPSYCHISME +PANSAMES +PANSE +PANSERAIS +PANSEUR +PANSLAVISMES +PANTACLE +PANTE +PANTELANTS +PANTELER +PANTELLERAIS +PANTENE +PANTHEISMES +PANTHEONISER +PANTIERES +PANTOIS +PANTOUFLARD +PANTOUFLAT +PANTOUFLERAS +PANTOUFLIERE +PANTYS +PAONNEAUX +PAPALE +PAPAUTES +PAPAYERS +PAPELARDS +PAPESSE +PAPETTE +PAPILIONEES +PAPILLIFERE +PAPILLONNERA +PAPILLONS +PAPILLOTANTE +PAPILLOTATES +PAPILLOTES +PAPISTE +PAPOTAIENT +PAPOTASSIONS +PAPOTERAIT +PAPOTIEZ +PAPOUS +PAPYROLOGIES +PAQUAGE +PAQUASSENT +PAQUEES +PAQUERETTE +PAQUETAGE +PAQUETASSENT +PAQUETERENT +PAQUETS +PAQUETTERIEZ +PAR +PARABENES +PARABOLIQUES +PARACHEVAIS +PARACHEVAT +PARACHEVERAI +PARACHUTAIT +PARACHUTERA +PARACHUTONS +PARADAIENT +PARADASSIONS +PARADERAIT +PARADEURS +PARADIS +PARADOXALE +PARAFAIT +PARAFATES +PARAFERAIT +PARAFEURS +PARAFFINANT +PARAFFINEE +PARAFFINONS +PARAFOUDRE +PARAISSANT +PARAITRAI +PARAITRONT +PARALLAXE +PARALOGIQUE +PARALYSAIT +PARALYSERAI +PARALYSERONT +PARAMES +PARAMETRAMES +PARAMETRE +PARAMETRERAS +PARAMETRIONS +PARAMNESIE +PARANGONNAGE +PARANGONNERA +PARANOIAQUES +PARANTHROPE +PARAPHATES +PARAPHERAIT +PARAPHERONS +PARAPHRASER +PARAPHRASONS +PARAPODES +PARAQUATS +PARASITAIRES +PARASITES +PARASITOIDE +PARASOL +PARASSIONS +PARATES +PARAVENT +PARCE +PARCELLARISA +PARCELLARISE +PARCELLAT +PARCELLERAI +PARCELLERONT +PARCHEMINANT +PARCHEMINEES +PARCHEMINEZ +PARCHETS +PARCOMETRE +PARCOURES +PARCOURRAIT +PARCOURUE +PARCOURUT +PARDONNAI +PARDONNERAI +PARDONNERONT +PAREDRE +PAREILS +PAREMENTANT +PAREMENTEE +PAREMENTONS +PARENT +PARENTE +PARENTS +PARERE +PARESIES +PARESSASSENT +PARESSERAI +PARESSERONT +PARESTHESIE +PAREUSE +PARFAISIONS +PARFASSIEZ +PARFERIONS +PARFILAMES +PARFILE +PARFILERAS +PARFILIONS +PARFIT +PARFONDES +PARFONDRAS +PARFONDUES +PARFUMANT +PARFUMEE +PARFUMERENT +PARFUMEURS +PARI +PARIAMES +PARIAT +PARIERAIS +PARIETAIRE +PARIEZ +PARIPENNEES +PARITAIRE +PARJURAIT +PARJURATES +PARJURERAIT +PARJURIEZ +PARKERISAMES +PARKERISEZ +PARLAIT +PARLASSENT +PARLEMENTA +PARLEMENTAS +PARLEMENTERA +PARLERAI +PARLERONT +PARLOIR +PARLOTAMES +PARLOTE +PARLOTEREZ +PARLOTTE +PARMENTIERS +PARNASSIENNE +PARODIAS +PARODIEES +PARODIEREZ +PARODIQUE +PARODONTIE +PAROISSES +PAROLES +PARONYME +PAROTIDES +PAROXYSMALE +PAROXYTONS +PARQUAIS +PARQUAT +PARQUERAI +PARQUERONT +PARQUETAMES +PARQUETE +PARQUETEUSE +PARQUETS +PARQUIER +PARRAINAGES +PARRAINASSES +PARRAINERA +PARRAINERONS +PARRAINONS +PARSEMAIENT +PARSEMES +PARSISME +PARTAGEAIT +PARTAGERAI +PARTAGERONT +PARTAIENT +PARTENAIRE +PARTERRES +PARTIALITES +PARTICIPATIF +PARTICIPERAI +PARTICULAIRE +PARTICULIERS +PARTINIUM +PARTIRENT +PARTISSIEZ +PARTITIFS +PARTITIONNEZ +PARTOUSAI +PARTOUSAS +PARTOUSER +PARTOUSIONS +PARTOUZARD +PARTOUZAT +PARTOUZERAS +PARTOUZEUSE +PARTURITIONS +PARULINE +PARURIERES +PARUTION +PARVENONS +PARVIENDRAS +PARVIENT +PARVINTES +PASCALS +PASHMINA +PASQUINAIS +PASQUINAT +PASQUINERAIS +PASQUINEZ +PASSACAILLES +PASSAI +PASSASSE +PASSAVANTS +PASSEMENTAI +PASSEMENTS +PASSEPOILAS +PASSEPOILENT +PASSEPORTS +PASSEREAU +PASSERONS +PASSEURS +PASSIFLORE +PASSIONISTES +PASSIONNANTE +PASSIONNATES +PASSIONNER +PASSIVAS +PASSIVE +PASSIVERAIT +PASSIVIEZ +PASTEL +PASTELLER +PASTELS +PASTEURISAIT +PASTEURISES +PASTICHAIENT +PASTICHES +PASTILLAGE +PASTILLER +PASTILLIONS +PASTISSAS +PASTISSEES +PASTISSEREZ +PASTORAL +PASTORIENNE +PATACA +PATAGON +PATAQUES +PATAS +PATAUGAS +PATAUGEANT +PATAUGEOIRE +PATAUGERENT +PATAUGEUSES +PATE +PATELINAIT +PATELINATES +PATELINERAIT +PATELINES +PATENE +PATENTAI +PATENTASSIEZ +PATENTERAI +PATENTERONT +PATER +PATERNELLE +PATEUSEMENT +PATHOGENE +PATI +PATIENTAIS +PATIENTAT +PATIENTERAIS +PATIENTEZ +PATINAI +PATINASSIEZ +PATINERAI +PATINERONT +PATINIONS +PATIRAIENT +PATIS +PATISSAS +PATISSEES +PATISSEREZ +PATISSIERE +PATITES +PATOISANT +PATOISAT +PATOISERAS +PATOISIONS +PATOUILLAIS +PATOUILLERAI +PATRAQUES +PATRIARCHE +PATRICIEN +PATRIGOTAI +PATRIGOTEZ +PATRILOCALE +PATRIMONIOS +PATRIOTISME +PATRONAL +PATRONNAGE +PATRONNER +PATRONNIERS +PATROUILLA +PATROUILLES +PATTE +PATTIERES +PATURABLES +PATURASSE +PATURENT +PATURERIEZ +PATURON +PAULIEN +PAULISTE +PAUMANT +PAUMEE +PAUMERAIT +PAUMIER +PAUMONS +PAUMOYASSENT +PAUMOYERENT +PAUPERISERA +PAUPIERES +PAUSAS +PAUSER +PAUSERIONS +PAUVRES +PAVAGE +PAVANAIS +PAVANAT +PAVANERAIS +PAVANEZ +PAVASSIONS +PAVERA +PAVERONS +PAVEUSES +PAVILLONNEUR +PAVLOVIENS +PAVOISASSE +PAVOISEMENT +PAVOISERENT +PAVOISONS +PAYA +PAYANTES +PAYE +PAYERAIS +PAYEUR +PAYSAGEE +PAYSAGISTES +PAYSES +PEANS +PEAUFINAIENT +PEAUFINES +PEBRINE +PECARI +PECCANTE +PECHAMES +PECHBLENDE +PECHERAIS +PECHERIEZ +PECHEUSE +PECLOTA +PECLOTASSES +PECLOTES +PECORE +PECTENS +PECTORALE +PECUNES +PEDAGOGUES +PEDALAS +PEDALEES +PEDALEREZ +PEDALEZ +PEDANTERIE +PEDEGERES +PEDESTRE +PEDICELLAIRE +PEDICULEES +PEDIEUSE +PEDIMENTS +PEDOLOGIES +PEDONCULEE +PEDZANT +PEDZENT +PEDZERIEZ +PEDZOUILLES +PEGOTS +PEGUAS +PEGUEES +PEGUEREZ +PEGUIEZ +PEIGNAIS +PEIGNAT +PEIGNERAIS +PEIGNETTE +PEIGNIMES +PEIGNITES +PEINAIENT +PEINAS +PEINDRAIENT +PEINE +PEINERAS +PEINIONS +PEINTURAGE +PEINTURER +PEINTURLURAI +PEJORASSE +PEJORATIONS +PEJORERAI +PEJORERONT +PEKIN +PEKINS +PELAGIE +PELAIENT +PELANTES +PELASGIENNE +PELAT +PELEENNES +PELERAS +PELERIONS +PELIONS +PELLAIENT +PELLASSIONS +PELLERAIENT +PELLES +PELLETANT +PELLETEE +PELLETEUSES +PELLETTERAIT +PELLICULAGE +PELLICULAS +PELLICULEES +PELLICULEREZ +PELLICULIEZ +PELOBATE +PELOTA +PELOTARIS +PELOTEE +PELOTERENT +PELOTEUSES +PELOTONNAMES +PELOTONNE +PELOTONNEZ +PELTAS +PELUCHAI +PELUCHASSIEZ +PELUCHERAI +PELUCHERONT +PELURES +PELVIMETRES +PEMPHIGOIDES +PENALISAIT +PENALISER +PENALITE +PENATES +PENCHAIS +PENCHASSIONS +PENCHERAIENT +PENCHES +PENDAIENT +PENDARDE +PENDERIES +PENDILLANT +PENDILLENT +PENDILLERIEZ +PENDIMES +PENDITES +PENDOUILLAIT +PENDOUILLE +PENDRAI +PENDRONT +PENDULAIS +PENDULAT +PENDULERAIS +PENDULETTE +PENDUS +PENETRABLE +PENETRANTE +PENETRATES +PENETRERA +PENETRERONS +PENIBILITES +PENICILLEES +PENINSULAIRE +PENITENT +PENNIES +PENNY +PENSABLES +PENSAS +PENSEES +PENSEREZ +PENSEUSES +PENSIONNERAI +PENSONS +PENTAGONES +PENTAPETALES +PENTARQUES +PENTATOME +PENTECOTES +PENTHODES +PENTOSES +PENTYS +PEPERIN +PEPIAMES +PEPIE +PEPIERAS +PEPIIONS +PEPITE +PEPPERMINTS +PEPTIDIQUE +PEQUENAUDES +PEQUISTE +PERCA +PERCALINES +PERCASSIEZ +PERCEPT +PERCEPTION +PERCERETTE +PERCETTE +PERCEVAIT +PERCEVRAIS +PERCHAGE +PERCHASSENT +PERCHEES +PERCHEREZ +PERCHEURS +PERCHLORATES +PERCIDE +PERCLUSES +PERCOLA +PERCOLASSES +PERCOLEE +PERCOLERENT +PERCOLONS +PERCUSSE +PERCUSSIVES +PERCUTANEES +PERCUTASSIEZ +PERCUTERAI +PERCUTERONT +PERDABLE +PERDENT +PERDISSES +PERDRA +PERDRIEZ +PERDUES +PERDURAS +PERDURER +PERDURERIONS +PEREGRIN +PEREGRINER +PEREMPTIONS +PERENNISA +PERENNISENT +PERENNITES +PERFECTIBLES +PERFECTIONS +PERFOLIE +PERFORAIT +PERFORASSIEZ +PERFORE +PERFORERAS +PERFOREZ +PERFORMANCES +PERFORMERA +PERFORMERONS +PERFORMIONS +PERFUSAMES +PERFUSE +PERFUSERAS +PERFUSION +PERIANTHE +PERIBOLES +PERICHONDRES +PERICLITERAI +PERIDERME +PERIDURALES +PERIGORDIEN +PERIHELIES +PERIMAIS +PERIMAT +PERIMERAIS +PERIMETRE +PERINATALES +PERINEAUX +PERIPHERIES +PERIPHRASENT +PERIPHS +PERIRAIT +PERISSEZ +PERITOINE +PERIURBAIN +PERLAIT +PERLASSENT +PERLEES +PERLEREZ +PERLINGUALE +PERLOT +PERMANENTANT +PERMANENTEES +PERMANGANATE +PERMETTAIENT +PERMETTONS +PERMETTRIONS +PERMISE +PERMISSIONS +PERMSELECTIF +PERMUTAIENT +PERMUTASSENT +PERMUTEES +PERMUTEREZ +PERNICIEUSE +PERONIER +PERORAIT +PERORATES +PERORERENT +PEROREUSES +PEROXYDAI +PEROXYDEES +PEROXYDEREZ +PEROXYSOME +PERPETRANT +PERPETRERAIT +PERPETRIEZ +PERPETUANT +PERPETUER +PERPIGNANAIS +PERRE +PERRUQUA +PERRUQUASSES +PERRUQUERA +PERRUQUERONS +PERRUQUONS +PERSECUTAIS +PERSECUTAT +PERSECUTEUR +PERSEIDES +PERSEVERANCE +PERSEVERERAI +PERSIENNE +PERSIFFLANT +PERSIFFLEE +PERSIFLAIS +PERSIFLAT +PERSIFLERAIS +PERSIFLEUR +PERSILLADE +PERSILLER +PERSILLEREZ +PERSILS +PERSISTANCES +PERSISTERAIT +PERSISTIEZ +PERSONNAGE +PERSONNIFIA +PERSONNIFIAT +PERSPICACES +PERSUADAMES +PERSUADE +PERSUADERAS +PERSUADIONS +PERSULFATES +PERTINENT +PERTURBAI +PERTURBASSE +PERTURBES +PERUVIENNE +PERVERSIONS +PERVERTIS +PERVIBRAGE +PERVIBRE +PERVIBRERAS +PERVIBRIONS +PESAIT +PESASSE +PESENT +PESERIEZ +PESEURS +PESONS +PESSIERE +PESTAIT +PESTATES +PESTERENT +PESTEZ +PESTILENTIEL +PETAGE +PETALE +PETANQUEURS +PETARADAI +PETARADASSE +PETARADERA +PETARADERONS +PETARDAIENT +PETARDES +PETAS +PETASSAMES +PETASSE +PETASSERAS +PETASSIONS +PETCHI +PETEES +PETEREZ +PETEUSES +PETILLANCE +PETILLASSIEZ +PETILLERAI +PETILLERONT +PETIONS +PETITESSES +PETITIONNANT +PETITOIRE +PETOCHARDS +PETONCLE +PETOUILLAS +PETOUILLER +PETRARQUISA +PETRARQUISAS +PETREL +PETRIFIAI +PETRIFIASSE +PETRIFIEE +PETRIFIERENT +PETRIFIONS +PETRIRAS +PETRISSAGE +PETRISSEURS +PETROCHIMIES +PETROGENESES +PETROLAI +PETROLASSIEZ +PETROLERAI +PETROLERONT +PETROLIERE +PETULANCES +PETUNAIT +PETUNATES +PETUNERENT +PETUNIEZ +PEUCEDANS +PEULS +PEUPLAMES +PEUPLE +PEUPLERAIENT +PEUPLERONT +PEUREUSEMENT +PEZES +PFUT +PHACOS +PHAGOCYTAGE +PHAGOCYTAS +PHAGOCYTEES +PHAGOCYTEREZ +PHAGOCYTOSE +PHALANGETTES +PHALANSTERES +PHALLINE +PHANATRONS +PHANOTRONS +PHANTASMER +PHARAMINEUX +PHARAONNE +PHARISAISMES +PHARYNGE +PHASMES +PHELLOGENES +PHENETIQUE +PHENIQUEES +PHENOLATE +PHENOMENAL +PHENOMENES +PHENOPLASTES +PHEROMONES +PHILIBEGS +PHILOLOGUE +PHILOSOPHENT +PHISHINGS +PHLEBOLOGUES +PHLEGMON +PHOBIES +PHOCIDIENNE +PHOLCODINE +PHONATION +PHONEMES +PHONETIQUES +PHONIQUES +PHONOGRAMME +PHONOLITHE +PHONOLOGUE +PHONOTHEQUES +PHOSGENE +PHOSPHATANT +PHOSPHATATES +PHOSPHATES +PHOSPHORASSE +PHOSPHOREMIE +PHOSPHORISME +PHOSPHORYLE +PHOSPHURE +PHOTOCHROME +PHOTOCOPIEE +PHOTOCOPIEZ +PHOTOGENE +PHOTOGLYPTIE +PHOTOMETRE +PHOTONIQUES +PHOTOPILE +PHOTOSPHERE +PHOTOTYPIES +PHRASAIT +PHRASATES +PHRASERA +PHRASERONS +PHRASONS +PHRENOLOGIES +PHRYGIENS +PHTIRIUS +PHTISIQUES +PHYCOMYCETES +PHYLLIE +PHYLLOXERA +PHYLUM +PHYSIATRIES +PHYSOSTIGMA +PHYTOCHIMIES +PHYTOGENE +PHYTOPHAGE +PHYTOPTE +PIACULAIRES +PIAFFANTES +PIAFFE +PIAFFERAS +PIAFFEURS +PIAILLAIENT +PIAILLASSE +PIAILLENT +PIAILLERIE +PIAILLEUSES +PIANISTES +PIANOTAGE +PIANOTASSENT +PIANOTER +PIANOTERIONS +PIAPIATAI +PIAPIATERAIS +PIAPIATEZ +PIAULAIENT +PIAULASSIONS +PIAULERAIENT +PIAULES +PIBALES +PICAILLON +PICAREL +PICASSIENS +PICHOLINES +PICKUP +PICOLAIT +PICOLATES +PICOLERAIT +PICOLEURS +PICONS +PICORASSENT +PICORER +PICORERIONS +PICOSSA +PICOSSASSES +PICOSSERA +PICOSSERONS +PICOTAGES +PICOTASSES +PICOTENT +PICOTERIEZ +PICOTONS +PICRIDE +PICROCHOLINS +PIDGIN +PIEDMONT +PIEGEA +PIEGEASSE +PIEGEONS +PIEGERIEZ +PIEGEZ +PIERCAI +PIERCASSIEZ +PIERCERAI +PIERCERONT +PIERCIONS +PIERREE +PIERRIERS +PIETAILLE +PIETASSIEZ +PIETER +PIETERIONS +PIETINAIS +PIETINASSES +PIETINENT +PIETINERIEZ +PIETIONS +PIETONNIERES +PIETONNISEES +PIETRAIN +PIEUTAI +PIEUTASSIEZ +PIEUTERAI +PIEUTERONT +PIEZES +PIFAS +PIFEES +PIFEREZ +PIFFAIS +PIFFAT +PIFFERAIS +PIFFEZ +PIFONS +PIGEAS +PIGENT +PIGEONNANT +PIGEONNAT +PIGEONNERAI +PIGEONNERONT +PIGERA +PIGERONS +PIGMENTAI +PIGMENTEES +PIGMENTEREZ +PIGMENTS +PIGNAMES +PIGNATELLES +PIGNERAIS +PIGNEZ +PIGNOCHAS +PIGNOCHEES +PIGNOCHEREZ +PIGNON +PIGOUILLA +PIGOUILLERA +PILAGE +PILAS +PILAU +PILER +PILERIONS +PILEUX +PILIPINO +PILLANT +PILLASSIONS +PILLERAIENT +PILLES +PILOCARPES +PILONNAIT +PILONNATES +PILONNERAIT +PILONNIEZ +PILOSELLE +PILOTAI +PILOTASSIEZ +PILOTERAI +PILOTERONT +PILOU +PILULES +PIMENTA +PIMENTASSE +PIMENTENT +PIMENTERIEZ +PIMPANT +PINACLES +PINAILLAMES +PINAILLE +PINAILLEREZ +PINAILLEZ +PINASTRE +PINCANT +PINCASSIONS +PINCEAUTAIS +PINCEAUTAT +PINCEAUTEZ +PINCEMENTS +PINCEREZ +PINCEUSE +PINCONS +PINDARISAMES +PINDARISE +PINDARISEREZ +PINDARISMES +PINENE +PINGRES +PINNULES +PINSON +PINTAIENT +PINTASSIONS +PINTERAIENT +PINTES +PINTOCHANT +PINTOCHENT +PINTOCHERIEZ +PINYIN +PIOCHANT +PIOCHEE +PIOCHERENT +PIOCHEUSES +PIONCAIENT +PIONCASSIONS +PIONCERAIENT +PIONCES +PIONNAI +PIONNASSIEZ +PIONNERAIS +PIONNEZ +PIORNAI +PIORNASSIEZ +PIORNERAIS +PIORNEZ +PIPAIT +PIPATES +PIPELINE +PIPERAIENT +PIPERIEZ +PIPES +PIPIDES +PIPIT +PIQUAGE +PIQUANTS +PIQUEE +PIQUENIQUES +PIQUEPOUL +PIQUEREZ +PIQUETAGE +PIQUETASSENT +PIQUETERENT +PIQUETS +PIQUETTERIEZ +PIQUEZ +PIQUOUZE +PIRATAIENT +PIRATASSIONS +PIRATERAIENT +PIRATERONS +PIRES +PIROJOKS +PIROUETTAMES +PIROUETTE +PIROUETTERAS +PIROUETTIONS +PISCICOLE +PISCINE +PISIFORME +PISSA +PISSASSE +PISSEES +PISSERAIS +PISSETTE +PISSOIR +PISSOTAS +PISSOTER +PISSOTERIONS +PISSOU +PISTAIENT +PISTASSE +PISTENT +PISTERIEZ +PISTIEZ +PISTOLAIT +PISTOLATES +PISTOLERAIT +PISTOLES +PISTOLIEZ +PISTONNAIT +PISTONNATES +PISTONNERAIT +PISTONNIEZ +PITBULL +PITCHOUNS +PITONNA +PITONNASSE +PITONNENT +PITONNERIEZ +PITONNIONS +PITPIT +PITTORESQUES +PITUITAMES +PITUITE +PITUITEREZ +PITUITIEZ +PIVOINES +PIVOTANTES +PIVOTE +PIVOTERAIS +PIVOTEZ +PIXELISAIT +PIXELISATES +PIXELISES +PIXELLISAMES +PIXELLISEZ +PIZZERIA +PLACAI +PLACARDAI +PLACARDERAI +PLACARDERONT +PLACARDISAIS +PLACAT +PLACEMENTS +PLACENTINES +PLACEREZ +PLACETTES +PLACIDITES +PLACOTA +PLACOTASSE +PLACOTENT +PLACOTERIEZ +PLACOTEZ +PLAFONNAGE +PLAFONNANTS +PLAFONNEE +PLAFONNERAIT +PLAFONNEURS +PLAGAUX +PLAGIAMES +PLAGIATS +PLAGIERAIT +PLAGIIEZ +PLAIDABLES +PLAIDAS +PLAIDEES +PLAIDEREZ +PLAIDEZ +PLAIES +PLAIGNARDES +PLAIGNIS +PLAINDRA +PLAINDRONS +PLAINTIVE +PLAIRE +PLAISAMMENT +PLAISANCIERS +PLAISANTASSE +PLAISANTENT +PLAISANTERIE +PLAISANTINS +PLAISIR +PLANAIRE +PLANCHAI +PLANCHASSIEZ +PLANCHEIAIS +PLANCHEIAT +PLANCHEIEZ +PLANCHERAIT +PLANCHETTE +PLANCTON +PLANEES +PLANERAIS +PLANETAIRE +PLANETOIDE +PLANEUSE +PLANIFIAIENT +PLANIFIEE +PLANIFIERENT +PLANIFIONS +PLANIPENNE +PLANNEUR +PLANORBES +PLANQUASSENT +PLANQUER +PLANQUERIONS +PLANSICHTERS +PLANTAINS +PLANTASSE +PLANTEE +PLANTERENT +PLANTEUSES +PLANTS +PLAQUAI +PLAQUASSIEZ +PLAQUEREZ +PLAQUEUR +PLAQUONS +PLASMIDES +PLASMIFIER +PLASMIQUE +PLASMODIUM +PLASTICAGES +PLASTIFIANT +PLASTIFIAT +PLASTIFIERAI +PLASTIQUAGES +PLASTIQUER +PLASTIQUIONS +PLASTRONNAIS +PLATANAIE +PLATEBANDES +PLATERESQUE +PLATIERE +PLATINAMES +PLATINE +PLATINERAS +PLATINIFERE +PLATINISAS +PLATINISEES +PLATINISEREZ +PLATINITE +PLATODES +PLATRA +PLATRASSE +PLATRENT +PLATRERIE +PLATREZ +PLAYMATE +PLEBE +PLEBISCITAT +PLEBISCITIEZ +PLEIADES +PLENIER +PLEONASTIQUE +PLETHORIQUE +PLEURAL +PLEURARDES +PLEURAUX +PLEURERAIT +PLEURESIES +PLEURITE +PLEURNICHANT +PLEURNICHAT +PLEURNICHES +PLEUTRES +PLEUVIOTA +PLEUVOTAT +PLEVRES +PLIAGES +PLIAS +PLIEES +PLIERAIENT +PLIES +PLINTHES +PLIQUE +PLISSAMES +PLISSE +PLISSERAIS +PLISSEUR +PLIURE +PLOIERA +PLOIERONT +PLOMBAGINEES +PLOMBASSENT +PLOMBEMIES +PLOMBEREZ +PLOMBEUSE +PLOMBIFERES +PLONGE +PLONGEANTS +PLONGEES +PLONGERAI +PLONGERONT +PLOTS +PLOYAIS +PLOYAT +PLOYIONS +PLUCHAS +PLUCHEES +PLUCHEREZ +PLUCHIEZ +PLUMAIS +PLUMASSENT +PLUMAT +PLUMER +PLUMERIONS +PLUMETS +PLUMIERS +PLURALE +PLURALISASSE +PLURALISEE +PLURALISME +PLURIACTIFS +PLURIANNUELS +PLURIEL +PLURILINGUE +PLURIVOQUE +PLUTES +PLUTONISME +PLUVIALES +PLUVINAIT +PNEU +PNEUMONIE +POACEE +POCHAIT +POCHARDANT +POCHARDEE +POCHARDERENT +POCHARDISE +POCHAT +POCHERAIS +POCHETEE +POCHEZ +POCHOTHEQUES +PODAGRE +PODCASTABLE +PODCASTER +PODCASTING +PODGORICIENS +PODIE +PODOLOGIQUE +PODZOL +PODZOLISAS +PODZOLISE +PODZOLISERAS +PODZOLISIONS +POELAGE +POELASSENT +POELER +POELERIES +POELIERS +POETEREAU +POETISAIENT +POETISERA +POETISERONS +POGNAIS +POGNAT +POGNERAIS +POGNEZ +POGROME +POIGNAIT +POIGNARDAIS +POIGNARDAT +POIGNARDEZ +POIGNASSIONS +POIGNERAIENT +POIGNES +POIGNISSENT +POILANT +POILAT +POILERAIS +POILEZ +POINCONNA +POINCONNASSE +POINDRAIT +POINS +POINTAIT +POINTAT +POINTENT +POINTERIEZ +POINTEUSE +POINTILLAIS +POINTILLAT +POINTILLEUSE +POINTILLONS +POINTUES +POIREAUTAIT +POIREAUTATES +POIREAUTONS +POIROTAIS +POIROTAT +POIROTERAS +POIROTIONS +POISONS +POISSARDES +POISSE +POISSERAS +POISSEUX +POISSONNIER +POITRAILS +POIVRAGE +POIVRASSENT +POIVRER +POIVRERIONS +POIVRIONS +POLACRE +POLARDS +POLARISAIS +POLARISASSES +POLARISES +POLDERIENS +POLDERISEES +POLDERISEREZ +POLDERS +POLEMIQUAIT +POLEMIQUATES +POLEMIQUONS +POLICAMES +POLICE +POLICERAIENT +POLICES +POLICLINIQUE +POLIOMYELITE +POLIRAIS +POLISSABLE +POLISSES +POLISSOIRES +POLISSONNAS +POLISSONNERA +POLISSURES +POLITICARDES +POLITIQUA +POLITIQUERAI +POLITISAIT +POLITISATES +POLITISES +POLLENS +POLLINISAI +POLLINISE +POLLINISERAS +POLLINISIONS +POLLUANT +POLLUAT +POLLUERAIS +POLLUEUR +POLO +POLOS +POLYAMINE +POLYARTHRITE +POLYCARPIQUE +POLYCHLORURE +POLYCOPIASSE +POLYCOPIENT +POLYCULTURE +POLYEDRE +POLYETHERS +POLYGAMIE +POLYGLOBULIE +POLYGYNE +POLYINSATURE +POLYMERASE +POLYMERISER +POLYMORPHES +POLYNEVRITES +POLYPHASEES +POLYPHONISTE +POLYPLOIDIE +POLYPOREES +POLYPTYQUE +POLYSEMIQUES +POLYSULFURES +POLYTHERME +POLYTRIC +POLYURIQUE +POMICULTURE +POMMADAS +POMMADEES +POMMADEREZ +POMMAI +POMMASSENT +POMMEES +POMMELASSENT +POMMELERENT +POMMELLERAIS +POMMENT +POMMERENT +POMMETES +POMMONS +POMOLOGIES +POMPAGES +POMPAS +POMPEES +POMPERAIS +POMPETTE +POMPIERE +POMPISTES +POMPONNASSE +POMPONNENT +POMPONNERIEZ +PONANT +PONCAIS +PONCAT +PONCER +PONCERIONS +PONCHO +PONCTIONNAT +PONCTIONNIEZ +PONCTUALITES +PONCTUATES +PONCTUELS +PONCTUEREZ +POND +PONDERALES +PONDERATES +PONDEREES +PONDEREREZ +PONDERIEZ +PONDIMES +PONDITES +PONDRE +PONDUS +PONGIDES +PONTAGES +PONTASSES +PONTENEGRIN +PONTERAIT +PONTETS +PONTIFIAI +PONTIFIASSE +PONTIFICAT +PONTIFIERAIT +PONTIFIIEZ +PONTON +POPAH +POPLITE +POPULACES +POPULARISA +POPULARISER +POQUAI +POQUASSIEZ +POQUERAI +POQUERONT +PORACEES +PORCELETS +PORCHES +POREUX +PORNOS +PORPHYRIES +PORQUES +PORRIDGES +PORTAGEAIENT +PORTAGES +PORTAIS +PORTANTES +PORTATIF +PORTEREZ +PORTEURS +PORTIEZ +PORTIONNES +PORTORICAINS +PORTRAITUREE +PORTUAIRE +POSADA +POSASSENT +POSEMETRE +POSERENT +POSEUSES +POSITIONNAI +POSITIVAMES +POSITIVE +POSITIVERAIT +POSITIVIEZ +POSITONIUM +POSOLOGIQUES +POSSEDANTES +POSSEDE +POSSEDERAS +POSSEDIONS +POSSIBILITES +POSTAIS +POSTASSES +POSTCOLONIAL +POSTDATANT +POSTDATEE +POSTDATERENT +POSTDATONS +POSTDOCTORAT +POSTENT +POSTERIEUR +POSTERISA +POSTERISERA +POSTERONT +POSTFACANT +POSTFACEE +POSTFACERENT +POSTFACONS +POSTICHES +POSTILLON +POSTPOSAMES +POSTPOSE +POSTPOSERAS +POSTPOSIONS +POSTULAMES +POSTULER +POSTULERIONS +POSTURALES +POTACHES +POTAMOCHERES +POTASSA +POTASSASSES +POTASSERA +POTASSERONS +POTASSIQUE +POTELE +POTENCEES +POTENTIEL +POTERIES +POTIER +POTINAIS +POTINAT +POTINERAS +POTINIERE +POTIQUETS +POTOROUS +POTTOKS +POUBELLE +POUCASSE +POUCERA +POUCERONS +POUCIERS +POUDRAGES +POUDRASSES +POUDRERA +POUDRERIEZ +POUDREZ +POUDROIEMENT +POUDROIERIEZ +POUDROYAIT +POUDROYATES +POUFFA +POUFFASSES +POUFFERAIENT +POUFFES +POUGNAIT +POUGNATES +POUGNERENT +POUGNONS +POUILLEUX +POULAGAS +POULARDE +POULETTES +POULINAI +POULINASSIEZ +POULINERAIS +POULINEZ +POULOTTE +POUND +POUPEES +POUPONNAGE +POUPONNER +POUPONS +POURCHASSA +POURCHASSES +POURFENDAIT +POURFENDIEZ +POURFENDIT +POURFENDREZ +POURGHERE +POURLECHANT +POURLECHEE +POURLECHONS +POURPREE +POURRAIENT +POURRIELS +POURRIRAIT +POURRISSE +POURRISSONS +POURSUITES +POURSUIVANT +POURSUIVI +POURSUIVRAIT +POURTOURS +POURVOIRAIT +POURVOIT +POURVOYIEZ +POURVUSSENT +POUSSAHS +POUSSASSES +POUSSERA +POUSSERONS +POUSSIERES +POUSSINES +POUSSONS +POUTOUNAIT +POUTOUNATES +POUTOUNERAIT +POUTOUNIEZ +POUTRELLES +POUTSASSE +POUTSENT +POUTSERIEZ +POUTURES +POUTZASSENT +POUTZER +POUTZERIONS +POUVAIT +POUZZOLANES +PRAESIDIUMS +PRAGOISES +PRAIRIAL +PRALINAI +PRALINASSIEZ +PRALINERAI +PRALINERONT +PRANDIALE +PRATIQUAI +PRATIQUASSE +PRATIQUEMENT +PRATIQUEREZ +PRAXIE +PREADHESIONS +PREALPINES +PREAVIS +PREAVISER +PREBENDEES +PRECARISERA +PRECATIFS +PRECAUTIONS +PRECEDASSENT +PRECEDENT +PRECEDERAS +PRECEDIONS +PRECEPTORAL +PRECESSION +PRECHANT +PRECHAUFFAGE +PRECHAUFFERA +PRECHEES +PRECHEREZ +PRECHEZ +PRECIPICE +PRECIPITAS +PRECIPITE +PRECIPITERAS +PRECIPITIONS +PRECISAIS +PRECISAT +PRECISES +PRECITEES +PRECOCITES +PRECOMMANDAI +PRECOMPTER +PRECONCEVRAI +PRECONCOIS +PRECONCUS +PRECONISEE +PRECONISIEZ +PRECUIRAIS +PRECUISAIENT +PRECUISIONS +PRECUISONS +PRECURSEURS +PREDATRICES +PREDECEDER +PREDECOUPAS +PREDECOUPENT +PREDEFINIES +PREDEFINIREZ +PREDESTINAIT +PREDESTINIEZ +PREDICABLE +PREDICATIONS +PREDICTIF +PREDILECTION +PREDIQUAS +PREDIQUEES +PREDIQUEREZ +PREDIRA +PREDIRIONS +PREDISES +PREDISPOSAT +PREDISPOSIEZ +PREDIT +PREDOMINANCE +PREDOMINEZ +PREEMBALLE +PREEMPTA +PREEMPTASSES +PREEMPTERA +PREEMPTERONS +PREEMPTIVES +PREETABLIS +PREEXISTAMES +PREEXISTES +PREFABRIQUEE +PREFACAI +PREFACASSIEZ +PREFACERAI +PREFACERONT +PREFECTORAL +PREFERAI +PREFERASSIEZ +PREFERENTIEL +PREFERERAS +PREFERIONS +PREFIGURERA +PREFINANCAIS +PREFINANCEZ +PREFIXAL +PREFIXER +PREFIXERIONS +PREFORMAIS +PREFORMAT +PREFORMATERA +PREFORMENT +PREFORMERIEZ +PREFORMONS +PREGENITAUX +PREHOMINIENS +PREINSCRITE +PREINSCRIVEZ +PREJUDICIAIS +PREJUDICIER +PREJUGEAIENT +PREJUGES +PRELASSAIT +PRELASSATES +PRELASSERAIT +PRELASSIEZ +PRELATURES +PRELAVAS +PRELAVEES +PRELAVEREZ +PRELE +PRELEVAS +PRELEVEES +PRELEVERAS +PRELEVIONS +PRELUDAIT +PRELUDATES +PRELUDERENT +PRELUDONS +PREMATURITE +PREMEDIQUANT +PREMEDIQUEES +PREMEDITAI +PREMEDITER +PREMENSTRUEL +PREMILITAIRE +PREMONITIONS +PREMOURANTS +PREMUNIRAIT +PREMUNISSAIS +PREMUNIT +PRENANTE +PRENDRAI +PRENDRONT +PRENNENT +PRENOMMAS +PRENOMMEES +PRENOMMEREZ +PRENOMS +PRENOTASSE +PRENOTEE +PRENOTERENT +PRENOTIONS +PREOBLITERE +PREOCCUPEZ +PREPAIENT +PREPAIERONS +PREPARAIS +PREPARAT +PREPARATRICE +PREPARERAIS +PREPAREZ +PREPAYAMES +PREPAYE +PREPAYERAS +PREPAYIONS +PREPONDERANT +PREPOSANT +PREPOSEE +PREPOSERENT +PREPOSITIF +PREPOTENCE +PREPSYCHOSE +PREPUCE +PRERAPPORT +PREREGLABLES +PREREGLASSE +PREREGLENT +PREREGLERIEZ +PREREINES +PRERETRAITE +PRESAGEAIT +PRESAGEATES +PRESAGERAIT +PRESAGIEZ +PRESBYTERAUX +PRESBYTIE +PRESCOLAIRES +PRESCRIREZ +PRESCRIVIONS +PRESCRIVONS +PRESENILE +PRESENTAMES +PRESENTATEUR +PRESENTEES +PRESENTERENT +PRESERVA +PRESERVASSES +PRESERVERA +PRESERVERONS +PRESIDAIS +PRESIDAT +PRESIDERA +PRESIDERONS +PRESIDIEZ +PRESOMPTIONS +PRESONORISEE +PRESSA +PRESSANTES +PRESSE +PRESSENTENT +PRESSENTIONS +PRESSENTIT +PRESSERENT +PRESSEUSES +PRESSIONNEES +PRESSURA +PRESSURASSE +PRESSURENT +PRESSURERIEZ +PRESSURIEZ +PRESSURISEES +PRESTA +PRESTAS +PRESTATION +PRESTERAIENT +PRESTES +PRESTIONS +PRESUMABLE +PRESUMASSENT +PRESUMER +PRESUMERIONS +PRESUPPOSAT +PRESUPPOSIEZ +PRESURAMES +PRESURE +PRESURERAS +PRESURIONS +PRETAMES +PRETATES +PRETENDANTES +PRETENDIS +PRETENDRAI +PRETENDRONT +PRETENTAINE +PRETER +PRETERIONS +PRETERITASSE +PRETERITENT +PRETERITS +PRETEXTA +PRETEXTASSES +PRETEXTERA +PRETEXTERONS +PRETIRASSE +PRETIRENT +PRETIRERIEZ +PRETOIRES +PRETRAILLE +PRETRANCHEES +PRETURES +PREVALENCES +PREVALONS +PREVALUSSIEZ +PREVARIQUA +PREVARIQUIEZ +PREVAUDRIEZ +PREVENANCES +PREVENTE +PREVENTIVES +PREVERBES +PREVIENDRONS +PREVINS +PREVOIENT +PREVOIRIEZ +PREVOTAUX +PREVOYANTE +PREVUS +PRIAMES +PRIAPISMES +PRIEE +PRIERE +PRIEURAL +PRIMA +PRIMALE +PRIMASSIEZ +PRIMATIES +PRIMAUX +PRIMERAIT +PRIMES +PRIMEVERES +PRIMIPARITE +PRIMITIVES +PRINCES +PRINCIPALATS +PRINCIPES +PRIORISAI +PRIORISER +PRIORITAIRES +PRISAMES +PRISERAIT +PRISEURS +PRISON +PRISSIONS +PRIVAIENT +PRIVASSIONS +PRIVATIONS +PRIVATISAMES +PRIVATISEZ +PRIVE +PRIVERAS +PRIVILEGE +PRIVILEGIER +PRO +PROBABILITE +PROBATION +PROBIOTIQUE +PROBLEME +PROCEDA +PROCEDASSES +PROCEDES +PROCEDURES +PROCESSIFS +PROCESSIVES +PROCHINOISE +PROCLAMAIT +PROCLAMATES +PROCLAMENT +PROCLAMERIEZ +PROCLISES +PROCONSULS +PROCRASTINAS +PROCRASTINER +PROCREAIT +PROCREATES +PROCREATIVES +PROCREES +PROCTOLOGIES +PROCURAIS +PROCURAT +PROCURATRICE +PROCURERAIS +PROCUREUR +PRODIGALITE +PRODIGUAIENT +PRODIGUES +PRODUCTEURS +PRODUCTIQUES +PRODUCTRICES +PRODUIRIONS +PRODUISES +PROEDRES +PROFANAIENT +PROFANEE +PROFANERENT +PROFANONS +PROFERAMES +PROFERE +PROFERERAS +PROFERIONS +PROFESSAS +PROFESSEES +PROFESSEREZ +PROFESSEZ +PROFILAIS +PROFILAT +PROFILERAIS +PROFILEUR +PROFILS +PROFITAIS +PROFITASSES +PROFITERONS +PROFITONS +PROFUS +PROGERIA +PROGLOTTIS +PROGRAMMAIS +PROGRAMMAT +PROGRAMMEURS +PROGRESSA +PROGRESSES +PROHIBAIS +PROHIBAT +PROHIBERAIS +PROHIBEZ +PROIE +PROJECTIONS +PROJETAIENT +PROJETEURS +PROJETTERA +PROJETTERONT +PROLAMINE +PROLETARIAT +PROLETARISEE +PROLIFERAI +PROLIFERASSE +PROLIFERIEZ +PROLINES +PROLOGUES +PROLONGERA +PROLONGERONS +PROMENAI +PROMENASSIEZ +PROMENERAI +PROMENERONT +PROMENOIRS +PROMETHEENS +PROMETTE +PROMETTONS +PROMETTRIONS +PROMIS +PROMISSIEZ +PROMOTION +PROMOTIONNAS +PROMOTRICE +PROMOUVONS +PROMOUVRONS +PROMPTS +PROMULGUA +PROMULGUERA +PROMUSSE +PRONAI +PRONASSES +PRONATRICES +PRONERAIT +PRONEURS +PRONOMINALES +PRONONCAMES +PRONONCE +PRONONCERAS +PRONOSTIQUEE +PROPAGEA +PROPAGEASSES +PROPAGERA +PROPAGERONS +PROPANIER +PROPHETIES +PROPHETISANT +PROPHETISEES +PROPHETISONS +PROPOSAIT +PROPOSASSIEZ +PROPOSERAI +PROPOSERONT +PROPRE +PROPRETTES +PROPULSAIS +PROPULSAT +PROPULSERAIS +PROPULSEUR +PROPULSONS +PROROGATIF +PROROGEAIS +PROROGEAT +PROROGERAIS +PROROGEZ +PROSATEURS +PROSCRIRIONS +PROSCRIVAIT +PROSCRIVIS +PROSECTEUR +PROSODIQUE +PROSPECT +PROSPECTER +PROSPECTION +PROSPECTUS +PROSPERERAI +PROSPERERONT +PROSTATE +PROSTERNA +PROSTERNONS +PROSTITUAMES +PROSTITUE +PROSTITUERAS +PROSTITUIONS +PROSTRES +PROTAMINES +PROTEAGINEUX +PROTECTORATS +PROTEGEAIENT +PROTEGES +PROTEINE +PROTEIQUE +PROTEOMIQUE +PROTEROGYNE +PROTESTAI +PROTESTANTS +PROTESTATES +PROTESTES +PROTHESE +PROTOCOLAI +PROTOCOLER +PROTOETOILE +PROTONEMA +PROTOPTERES +PROTOTYPES +PROTOXYDES +PROUT +PROUVANT +PROUVEE +PROUVERENT +PROUVONS +PROVENCALE +PROVENIEZ +PROVERBIAL +PROVIENDRONS +PROVIGNAI +PROVIGNER +PROVINCES +PROVINRENT +PROVISEUR +PROVISIONNEL +PROVOC +PROVOQUANT +PROVOQUEE +PROVOQUERENT +PROVOQUONS +PROXIMAL +PRUCHE +PRUDENTIELLE +PRUDHOMMAUX +PRUINES +PRUNELEES +PRUNIER +PRUSSE +PRUSSIQUE +PSALMISTE +PSALMODIASSE +PSALMODIENT +PSALMODIQUES +PSCHENTS +PSEUDOMONAS +PSI +PSILOTUMS +PSOAS +PSST +PSYCHIATRES +PSYCHOGENE +PSYLLE +PTEROSAURE +PTERYGOTE +PTYALINE +PUAMES +PUASSES +PUBERTAIRE +PUBIEN +PUBLIAIS +PUBLIAT +PUBLICISTES +PUBLIER +PUBLIERIONS +PUBLIPHONE +PUBLIVORES +PUCANT +PUCCINIAS +PUCELLE +PUCERENT +PUCHES +PUDDINGS +PUDDLAS +PUDDLEES +PUDDLEREZ +PUDDLIONS +PUDICITE +PUENT +PUERICULTEUR +PUERILES +PUERPERALE +PUGILISTES +PUINEES +PUISAIT +PUISASSIONS +PUISENT +PUISERIEZ +PUISONS +PUISSENT +PULICAIRES +PULLULAIS +PULLULAT +PULLULERAI +PULLULERONT +PULPAIRE +PULPITES +PULSANTE +PULSASSIONS +PULSATILLES +PULSENT +PULSERIEZ +PULTACEES +PULVERISENT +PULVERISONS +PUNA +PUNAISASSENT +PUNAISER +PUNAISERIONS +PUNCHES +PUNICACEE +PUNIRAIENT +PUNIS +PUNISSEUR +PUNITIF +PUNT +PUPE +PUPIPARE +PUREAU +PURGATIFS +PURGEAIENT +PURGEASSIONS +PURGERA +PURGERONS +PURIFIAIENT +PURIFIASSENT +PURIFIERAI +PURIFIERONT +PURINAIS +PURINAT +PURINERAIS +PURINEZ +PURISTES +PUROTIN +PURPURIQUES +PUSEYISMES +PUSSIEZ +PUSTULOSES +PUTASSANT +PUTASSENT +PUTASSERIEZ +PUTASSIEZ +PUTIER +PUTREFIABLE +PUTREFIER +PUTRESCENT +PUTRIDE +PUTTA +PUTTASSES +PUTTERA +PUTTERONS +PUTTO +PUYS +PYCNOSES +PYGMALIONS +PYLONES +PYOGENES +PYRAMIDAI +PYRAMIDASSE +PYRAMIDEES +PYRAMIDEREZ +PYRAMIDONS +PYRENEISTE +PYRETHRINES +PYRIDOXAL +PYROGRAVAI +PYROGRAVERAI +PYROGRAVURE +PYROLYSAI +PYROLYSERAI +PYROLYSERONT +PYROMANIE +PYROPHORE +PYROSIS +PYROTECHNIES +PYROXYLES +PYRRHONISME +PYRUVIQUE +PYTHIEN +PYTHONISSE +QANOUNS +QATARIEN +QING +QUADRA +QUADRANGLES +QUADRATURE +QUADRIENNAL +QUADRIGE +QUADRILLA +QUADRILLASSE +QUADRILLENT +QUADRILOBE +QUADRIPARTI +QUADRIPLACE +QUADRIQUE +QUADRUPEDES +QUADRUPLAS +QUADRUPLEES +QUADRUPLERAS +QUADRUPLEX +QUAI +QUALIFIABLE +QUALIFIANTS +QUALIFIENT +QUALIFIERIEZ +QUALITATIF +QUALITICIENS +QUANTIFIEE +QUANTIFIONS +QUANTITES +QUARANTIEME +QUARDERONNEZ +QUART +QUARTAGEAS +QUARTAGENT +QUARTAGEREZ +QUARTAIENT +QUARTASSE +QUARTAUTS +QUARTENT +QUARTERIEZ +QUARTETTE +QUARTILAGE +QUARTZ +QUASAR +QUASSIERS +QUATORZE +QUATRILLION +QUEBECISES +QUEBRACHO +QUELLES +QUEMANDAMES +QUEMANDE +QUEMANDERAS +QUEMANDEUSE +QUENETTE +QUERABLE +QUERCITRONS +QUERELLANT +QUERELLEE +QUERELLERENT +QUERELLEUSES +QUERULENTES +QUESTIONNAT +QUESTIONNES +QUESTURE +QUETAIT +QUETATES +QUETERAIT +QUETEURS +QUETSCHIER +QUEUSOTS +QUEUTAS +QUEUTER +QUEUTERIONS +QUIA +QUICKS +QUIESCENTE +QUIETS +QUILLAMES +QUILLAT +QUILLERAIS +QUILLEUR +QUILLONS +QUINAIRES +QUINCYS +QUINIDINES +QUINOLEIQUES +QUINQUAS +QUINQUINA +QUINTIL +QUINTOIERAIS +QUINTOLETS +QUINTOYIEZ +QUINTUPLANT +QUINTUPLEE +QUINTUPLONS +QUIQUAGEONS +QUISCALES +QUITTAMES +QUITTANCER +QUITTASSE +QUITTENT +QUITTERIEZ +QUIZ +QUOTIDIENNE +QWERTY +RABACHAGES +RABACHASSES +RABACHENT +RABACHERIEZ +RABACHIEZ +RABAISSANT +RABAISSEE +RABAISSERAIT +RABAISSIEZ +RABASSES +RABATTANT +RABATTEURS +RABATTISSENT +RABATTRAI +RABATTRONT +RABBINIQUES +RABIBOCHAMES +RABIBOCHE +RABIBOCHERAS +RABIBOCHIONS +RABIOTAIS +RABIOTAT +RABIOTERAIS +RABIOTEZ +RABLAIS +RABLAT +RABLERAIS +RABLEZ +RABONNIR +RABONNIRIONS +RABONNISSES +RABOTAGES +RABOTASSES +RABOTENT +RABOTERIEZ +RABOTEZ +RABOUDINAIS +RABOUDINAT +RABOUDINEZ +RABOUGRIRAI +RABOUGRIRONT +RABOUGRISSEZ +RABOUILLEUSE +RABOUTANT +RABOUTEE +RABOUTERENT +RABOUTONS +RABROUASSENT +RABROUEMENTS +RABROUEREZ +RABROUEZ +RACAHOUTS +RACCOMMODAI +RACCOMMODERA +RACCOMPAGNA +RACCOMPAGNAT +RACCOMPAGNEZ +RACCORDAMES +RACCORDE +RACCORDERAIS +RACCORDEZ +RACCOURCIRA +RACCOUTUMEE +RACCROC +RACCROCHAS +RACCROCHEES +RACCROCHERAS +RACCROCHEUSE +RACCUSAIS +RACCUSAT +RACCUSERAIS +RACCUSEZ +RACEMIQUE +RACHETABLE +RACHETASSENT +RACHETER +RACHETERIONS +RACHIALGIES +RACHITIQUES +RACINAGES +RACINAS +RACINEE +RACINERENT +RACINIENNES +RACISTES +RACKETTAIENT +RACKETTERS +RACKS +RACLAS +RACLEES +RACLERAS +RACLEUR +RACLURE +RACOLANT +RACOLEE +RACOLERENT +RACOLEUSES +RACONTAIS +RACONTASSIEZ +RACONTERAI +RACONTERONT +RACOON +RACORNIRAIS +RACORNISSIEZ +RACRAPOTAMES +RACRAPOTE +RACRAPOTERAS +RACRAPOTIONS +RADAR +RADASSIONS +RADERA +RADERONS +RADIAIRES +RADIANS +RADIASSIONS +RADIATIVES +RADICALISAT +RADICALISES +RADICANTE +RADICULALGIE +RADIER +RADIERIONS +RADIN +RADINASSENT +RADINER +RADINERIES +RADINISMES +RADIOCHIMIE +RADIODIFFUSA +RADIODIFFUSE +RADIOELEMENT +RADIOGRAMMES +RADIOGUIDAI +RADIOGUIDEZ +RADIOLOGIE +RADIOPHARE +RADIOREVEILS +RADIOSONDAGE +RADIQUES +RADJAS +RADOTAI +RADOTASSIEZ +RADOTERAI +RADOTERONT +RADOUB +RADOUBAS +RADOUBEES +RADOUBEREZ +RADOUBS +RADOUCIRAIT +RADOUCISSAIS +RAFALAI +RAFALASSIEZ +RAFALERAIS +RAFALEZ +RAFFERMIRAI +RAFFERMIRONT +RAFFINAGE +RAFFINASSENT +RAFFINEMENT +RAFFINERENT +RAFFINEURS +RAFFLES +RAFFOLAIS +RAFFOLAT +RAFFOLERAS +RAFFOLIONS +RAFFUTAMES +RAFFUTE +RAFFUTERAS +RAFFUTIONS +RAFISTOLAI +RAFISTOLERAI +RAFLAIT +RAFLATES +RAFLERAIT +RAFLIEZ +RAFRAICHIS +RAFTEUR +RAGAILLARDI +RAGEANTE +RAGEATES +RAGERENT +RAGEUSEMENT +RAGONDIN +RAGOTAS +RAGOTER +RAGOTERIONS +RAGOUGNASSES +RAGRAFAIS +RAGRAFAT +RAGRAFERAIS +RAGRAFEZ +RAGREAIT +RAGREATES +RAGREERAIT +RAGREIEZ +RAGUAIS +RAGUAT +RAGUERAIS +RAGUEZ +RAIDERS +RAIDIR +RAIDIRIONS +RAIDITES +RAIEREZ +RAILLAI +RAILLASSIEZ +RAILLERAI +RAILLERIONS +RAILLIEZ +RAINAIT +RAINATES +RAINERAIT +RAINETTES +RAINURAIS +RAINURAT +RAINURERAIS +RAINUREZ +RAIRAIT +RAISINE +RAISONNANTS +RAISONNEE +RAISONNERAIT +RAISONNEURS +RAJAH +RAJEUNIRA +RAJEUNIRONS +RAJEUNISSE +RAJEUNITES +RAJOUTANT +RAJOUTEE +RAJOUTERENT +RAJOUTONS +RAJUSTANT +RAJUSTEE +RAJUSTERAIT +RAJUSTIEZ +RALAIS +RALASSES +RALENTIE +RALENTIRENT +RALENTISSANT +RALERAS +RALEUSE +RALINGUANT +RALINGUEE +RALINGUERENT +RALINGUONS +RALLASSE +RALLERA +RALLERONS +RALLIANT +RALLIDES +RALLIERAIENT +RALLIES +RALLONGERA +RALLONGERONS +RALLUMAIS +RALLUMAT +RALLUMERAIS +RALLUMEZ +RAMADANESQUE +RAMAGEANT +RAMAGEES +RAMAGERENT +RAMAI +RAMANCHAMES +RAMANCHE +RAMANCHERAS +RAMANCHEUSE +RAMAS +RAMASSAS +RAMASSEES +RAMASSERAS +RAMASSEUR +RAMASSONS +RAMDAMS +RAMENAIS +RAMENASSENT +RAMENDAIT +RAMENDATES +RAMENDERAIT +RAMENDEURS +RAMENENT +RAMENERIEZ +RAMENS +RAMERAS +RAMEROTS +RAMEUTA +RAMEUTASSES +RAMEUTERA +RAMEUTERONS +RAMIE +RAMIFIAMES +RAMIFICATION +RAMIFIERAIS +RAMIFIEZ +RAMIONS +RAMOLLIRAIS +RAMOLLISSENT +RAMONA +RAMONASSE +RAMONENT +RAMONERIEZ +RAMONONS +RAMPANTES +RAMPE +RAMPERAIS +RAMPEZ +RAMULES +RANCARDAIENT +RANCARDES +RANCESCIBLE +RANCI +RANCIRAIS +RANCISSAIENT +RANCISSIEZ +RANCONNA +RANCONNASSES +RANCONNENT +RANCONNERIEZ +RANCONNIEZ +RANCUNIERE +RANDOMISAMES +RANDOMISEZ +RANDONNANT +RANDONNEE +RANDONNERENT +RANDONNEUSES +RANGEAI +RANGEASSIEZ +RANGER +RANGERIONS +RANIDE +RANIMANT +RANIMATIONS +RANIMERAIT +RANIMIEZ +RAPACE +RAPAILLAIENT +RAPAILLES +RAPASSE +RAPATRIAGE +RAPATRIEREZ +RAPATRONNA +RAPATRONNER +RAPENT +RAPERCHAIENT +RAPERCHES +RAPERIONS +RAPETASSAIT +RAPETASSATES +RAPETASSIEZ +RAPETISSAS +RAPETISSEES +RAPETISSERAS +RAPETISSIONS +RAPHAELIQUE +RAPIA +RAPICOLAIT +RAPICOLATES +RAPICOLERAIT +RAPICOLIEZ +RAPIECA +RAPIECASSE +RAPIECEMENT +RAPIECERENT +RAPIECONS +RAPINAIS +RAPINAT +RAPINERAIS +RAPINERONT +RAPLATI +RAPLATIRAS +RAPLATISSAIT +RAPLATITES +RAPLOMBER +RAPOINTIES +RAPOINTIREZ +RAPOINTISSE +RAPPA +RAPPAREILLEE +RAPPARIAI +RAPPARIER +RAPPASSENT +RAPPELA +RAPPELASSE +RAPPELER +RAPPELONS +RAPPERENT +RAPPEUSE +RAPPLIQUAMES +RAPPLIQUE +RAPPLIQUERAS +RAPPLIQUIONS +RAPPOINTIT +RAPPONDEZ +RAPPONDRE +RAPPONDUS +RAPPORTAI +RAPPORTERAI +RAPPORTERONT +RAPPORTIONS +RAPPRENDS +RAPPRETAIENT +RAPPRETES +RAPPRISSE +RAPPROCHAI +RAPPROCHER +RAPPROPRIAT +RAPPROPRIIEZ +RAPPUIEREZ +RAPPUYAIT +RAPPUYATES +RAPPUYONS +RAPTUS +RAQUAS +RAQUEES +RAQUEREZ +RAQUETTEUSE +RAREFIABLE +RAREFIASSENT +RAREFIER +RAREFIERIONS +RARESCENT +RASADE +RASANCES +RASASSIONS +RASENT +RASERIEZ +RASETTES +RASIEZ +RASSASIAI +RASSASIASSE +RASSASIEMENT +RASSASIERENT +RASSASIONS +RASSEMBLEREZ +RASSEMBLEZ +RASSERENAMES +RASSERENE +RASSERENERAS +RASSERENIONS +RASSEYIEZ +RASSIERAIS +RASSIR +RASSIRIONS +RASSISSE +RASSITES +RASSOIREZ +RASSORTIMENT +RASSORTIRENT +RASSOUL +RASSURA +RASSURAS +RASSUREES +RASSUREREZ +RASTA +RATAFIA +RATAPLAN +RATATINAI +RATATINERAI +RATATINERONT +RATATOUILLER +RATEES +RATELANT +RATELEE +RATELIER +RATELLERAIT +RATELURES +RATEREZ +RATIBOISAIS +RATIBOISAT +RATIBOISEZ +RATIER +RATIFIAMES +RATIFICATION +RATIFIERAIS +RATIFIEZ +RATINAIT +RATINATES +RATINERAIT +RATINEUSES +RATIOCINES +RATIONAL +RATIONALISEZ +RATIONAUX +RATIONNAS +RATIONNEES +RATIONNERA +RATIONNERONS +RATISSAGE +RATISSASSENT +RATISSER +RATISSERIONS +RATISSOIRES +RATONNAI +RATONNASSIEZ +RATONNERAI +RATONNERONT +RATONS +RATTACHAIS +RATTACHAT +RATTACHERAI +RATTACHERONT +RATTAQUA +RATTAQUASSES +RATTAQUERA +RATTAQUERONS +RATTRAPABLE +RATTRAPAS +RATTRAPEES +RATTRAPEREZ +RATURA +RATURASSE +RATURENT +RATURERIEZ +RAUBASINES +RAUCHAS +RAUCHEES +RAUCHEREZ +RAUCHIONS +RAUGMENTANT +RAUGMENTEE +RAUGMENTONS +RAUQUASSENT +RAUQUER +RAUQUERIONS +RAVAGE +RAVAGEASSENT +RAVAGER +RAVAGERIONS +RAVAGIONS +RAVALASSE +RAVALEMENT +RAVALERENT +RAVALIEZ +RAVAUDAIT +RAVAUDATES +RAVAUDERAIT +RAVAUDEURS +RAVENALA +RAVEURS +RAVIGOTA +RAVIGOTAS +RAVIGOTEES +RAVIGOTEREZ +RAVILI +RAVILIRAS +RAVILISSAIT +RAVILITES +RAVINAS +RAVINEES +RAVINERAS +RAVINIONS +RAVIRAIENT +RAVIS +RAVISASSENT +RAVISER +RAVISERIONS +RAVISSAIT +RAVISSEUR +RAVITAILLAI +RAVITAILLERA +RAVITES +RAVIVAS +RAVIVEES +RAVIVEREZ +RAVOIR +RAYANT +RAYEE +RAYERAIT +RAYES +RAYONNAI +RAYONNASSE +RAYONNEMENT +RAYONNERENT +RAYONNIEZ +RAYURES +RAZZIAIT +RAZZIATES +RAZZIERAIT +RAZZIIEZ +REABONNAI +REABONNER +REABSORBES +REACCLIMATE +REACCOUTUMA +REACCOUTUMEZ +REACTEURS +REACTIONS +REACTIVEES +REACTIVEREZ +REACTIVITES +REACTUALISEE +READAPTAI +READAPTER +READMETTONS +READMISSES +REAFFECTAIT +REAFFECTATES +REAFFECTES +REAFFILIAMES +REAFFILIE +REAFFILIERAS +REAFFILIIONS +REAFFIRMASSE +REAFFIRMEE +REAFFIRMONS +REAGENCEREZ +REAGI +REAGIRAS +REAGISSAIT +REAGITES +REAJUSTAMES +REAJUSTE +REAJUSTERAIS +REAJUSTEZ +REALESAI +REALESASSIEZ +REALESERAI +REALESERONT +REALIGNAIENT +REALIGNERA +REALIGNERONS +REALIMENTAIS +REALISANT +REALISATEURS +REALISERA +REALISERONS +REALISTEMENT +REAMENAGEA +REAMENAGER +REAMORCAIENT +REAMORCES +REANIMAIS +REANIMAT +REANIMEES +REANIMEREZ +REANT +REAPPARURENT +REAPPRENAIS +REAPPRENDRE +REAPPRENNE +REAPPRISSENT +REAPPROPRIEE +REARGENTER +REARMAIENT +REARMASSIONS +REARMERA +REARMERONS +REARRANGEAT +REARRANGERAI +REASSIGNA +REASSIGNENT +REASSORT +REASSORTIS +REASSURANCE +REASSURATES +REASSURERAIT +REASSUREURS +REATTRIBUAIS +REBAISSAIENT +REBAISSES +REBAPTISAMES +REBAPTISE +REBAPTISERAS +REBAPTISIONS +REBATIMES +REBATIRIEZ +REBATISSENT +REBATTAIS +REBATTIMES +REBATTITES +REBATTRIEZ +REBELLA +REBELLASSES +REBELLERA +REBELLERONS +REBETIKO +REBIFFAIS +REBIFFAT +REBIFFERAIS +REBIFFEZ +REBIQUANT +REBIQUENT +REBIQUERIEZ +REBLANCHIE +REBLANCHISSE +REBLOCHONS +REBOBINER +REBOIRAIENT +REBOIS +REBOISASSENT +REBOISEMENTS +REBOISEREZ +REBOIT +REBONDIMES +REBONDIRIEZ +REBONDS +REBOOTAS +REBOOTEES +REBOOTEREZ +REBORD +REBORDASSENT +REBORDER +REBORDERIONS +REBOTS +REBOUCHAS +REBOUCHEES +REBOUCHEREZ +REBOUILLEUR +REBOUTAS +REBOUTEES +REBOUTERAS +REBOUTEUSE +REBOUTONNAIT +REBOUTONNE +REBOUTONNONS +REBRAGUETTEZ +REBRANCHANT +REBRANCHEE +REBRANCHONS +REBRODASSE +REBRODENT +REBRODERIEZ +REBROUSSAI +REBROUSSER +REBRULAIENT +REBRULES +REBUMES +REBUT +REBUTANTS +REBUTEE +REBUTERENT +REBUTONS +RECACHETA +RECACHETES +RECADRAGES +RECADRASSES +RECADRERA +RECADRERONS +RECALAI +RECALASSIEZ +RECALCIFIANT +RECALCIFIEE +RECALCITRANT +RECALCULAMES +RECALCULE +RECALCULERAS +RECALCULIONS +RECALERAIENT +RECALES +RECALIBRANT +RECALIBREE +RECALIBRONS +RECAPITULAT +RECAPITULEES +RECARDAMES +RECARDE +RECARDERAS +RECARDIONS +RECARRELANT +RECARRELEE +RECARRELLERA +RECARRELLES +RECASASSE +RECASEMENT +RECASERENT +RECASONS +RECASSASSENT +RECASSER +RECASSERIONS +RECAUSAIENT +RECAUSERAIT +RECAUSIEZ +RECAVAS +RECAVEES +RECAVEREZ +RECEDA +RECEDASSES +RECEDERA +RECEDERONS +RECELAIENT +RECELASSIONS +RECELERAIENT +RECELES +RECEMMENT +RECENSAS +RECENSEES +RECENSERAS +RECENSEUSE +RECENTRA +RECENTRASSE +RECENTRENT +RECENTRERIEZ +RECEPA +RECEPASSE +RECEPENT +RECEPERIEZ +RECEPONS +RECEPTIONNAT +RECEPTRICES +RECERCLAS +RECERCLEES +RECERCLEREZ +RECES +RECETTES +RECEVANTES +RECEVONS +RECEVRONS +RECHAMPIS +RECHAMPISSEZ +RECHANGEAIS +RECHANGEAT +RECHANGERAIS +RECHANGEZ +RECHANTAS +RECHANTEES +RECHANTEREZ +RECHAPA +RECHAPASSE +RECHAPENT +RECHAPERIEZ +RECHAPPAI +RECHAPPERAI +RECHAPPERONT +RECHARGEAI +RECHARGER +RECHASSAIS +RECHASSAT +RECHASSERAIS +RECHASSEZ +RECHAUFFERA +RECHAUFFONS +RECHAUSSEREZ +RECHE +RECHERCHER +RECHES +RECHIGNEREZ +RECHUTAI +RECHUTASSIEZ +RECHUTERAIS +RECHUTEZ +RECIDIVANT +RECIDIVAT +RECIDIVERAIS +RECIDIVEZ +RECIF +RECIPIENT +RECIPROQUANT +RECIPROQUEES +RECIT +RECITANTE +RECITATES +RECITERA +RECITERONS +RECLAMA +RECLAMAS +RECLAME +RECLAMERAS +RECLAMIONS +RECLASSASSE +RECLASSEMENT +RECLASSERENT +RECLASSONS +RECLOUASSENT +RECLOUER +RECLOUERIONS +RECLUSES +RECOGNITIVES +RECOIFFER +RECOIS +RECOLAMES +RECOLE +RECOLERAIS +RECOLEZ +RECOLLAMES +RECOLLE +RECOLLERAI +RECOLLERONT +RECOLTABLE +RECOLTANTS +RECOLTEE +RECOLTERENT +RECOLTEUSES +RECOMBINERA +RECOMMANDAI +RECOMMANDER +RECOMMENCAIS +RECOMMENCEZ +RECOMPENSAIT +RECOMPENSE +RECOMPENSONS +RECOMPILER +RECOMPOSERA +RECOMPTAGE +RECOMPTER +RECONCENTRE +RECONCILIA +RECONDAMNER +RECONDUIS +RECONDUISONS +RECONFIRMAT +RECONFIRMIEZ +RECONFORTANT +RECONGELAS +RECONGELEES +RECONGELEREZ +RECONNAIS +RECONNAITRA +RECONNECTER +RECONNUMES +RECONQUERRAI +RECONQUETE +RECONQUISE +RECONSIDERAI +RECONSOLIDEZ +RECONSTITUEE +RECONTACTAIS +RECONVOQUEE +RECOPIA +RECOPIASSE +RECOPIENT +RECOPIERIEZ +RECOQUILLAI +RECOQUILLEZ +RECORDAIS +RECORDAT +RECORDERAIS +RECORDEZ +RECORDWOMEN +RECORRIGER +RECOUCHAIENT +RECOUCHES +RECOUDRAIT +RECOUPAGE +RECOUPASSENT +RECOUPEMENTS +RECOUPEREZ +RECOUPIONS +RECOUPONNERA +RECOURANT +RECOURBANT +RECOURBEE +RECOURBERAIT +RECOURBIEZ +RECOURIONS +RECOURRIEZ +RECOURURENT +RECOUSAIS +RECOUSIRENT +RECOUSU +RECOUVRABLES +RECOUVRASSE +RECOUVREMENT +RECOUVRERENT +RECOUVRIONS +RECOUVRIRIEZ +RECOUVRIT +RECRACHAS +RECRACHEES +RECRACHEREZ +RECRE +RECREAS +RECREATION +RECREERAI +RECREERONT +RECREPIES +RECREPIREZ +RECREPISSAIT +RECREPITES +RECREUSASSE +RECREUSENT +RECREUSERIEZ +RECRIAI +RECRIASSIEZ +RECRIERAI +RECRIERONT +RECRIMINAMES +RECRIMINER +RECRIRAI +RECRIRONT +RECRITE +RECRIVENT +RECRIVISSES +RECROISAIS +RECROISAT +RECROISERAIS +RECROISEZ +RECROISSES +RECROITRAIT +RECRUDESCENT +RECRUSSENT +RECRUTAMES +RECRUTE +RECRUTERAIS +RECRUTEUR +RECTALE +RECTIFIABLE +RECTIFIER +RECTIFIIONS +RECTITUDE +RECTORAUX +RECUEILLAIT +RECUEILLIE +RECUIRAIENT +RECUIS +RECUISIMES +RECUISITES +RECULADE +RECULASSENT +RECULEMENTS +RECULEREZ +RECULON +RECULOTTASSE +RECULOTTENT +RECUMES +RECUPERANT +RECUPERERA +RECUPERERONS +RECURAGES +RECURASSES +RECURERA +RECURERONS +RECURRENTE +RECURSOIRES +RECUSANT +RECUSATIONS +RECUSERAIT +RECUSIEZ +RECYCLA +RECYCLAIT +RECYCLATES +RECYCLERAIT +RECYCLEURS +REDACTIONNEL +REDDITIONS +REDECOLLAS +REDECOLLER +REDECORAIENT +REDECORES +REDECOUPAIS +REDECOUPAT +REDECOUPEZ +REDECOUVRAIT +REDECOUVRIRA +REDEFAIS +REDEFAITES +REDEFERAIS +REDEFINIE +REDEFINIRENT +REDEFINITION +REDEFITES +REDEMANDASSE +REDEMANDENT +REDEMARRAGE +REDEMARRER +REDEMPTION +REDENTEES +REDEPLOYAI +REDEPLOYEZ +REDEPOSANT +REDEPOSEE +REDEPOSERENT +REDEPOSONS +REDESCENDIEZ +REDESSINAI +REDESSINERAI +REDEVAIT +REDEVENIONS +REDEVIENNENT +REDEVINSSES +REDEVRAIENT +REDHIBITIONS +REDIFFUSES +REDIGEAIENT +REDIGERAIENT +REDIGES +REDIMANT +REDIMEE +REDIMER +REDIMERIONS +REDIRECTIONS +REDIRIGEAMES +REDIRIGEE +REDIRIGERAS +REDIRIGIONS +REDISCUTAI +REDISCUTERAI +REDISEURS +REDISSIONS +REDOIVES +REDONDANTE +REDONDATES +REDONDERENT +REDONDONS +REDONNASSENT +REDONNER +REDONNERIONS +REDORAIENT +REDORASSIONS +REDORERAIENT +REDORES +REDORMES +REDORMIRAI +REDORMIRONT +REDORONS +REDOUBLANTE +REDOUBLATES +REDOUBLES +REDOUTABLES +REDOUTASSES +REDOUTERA +REDOUTERONS +REDOX +REDRESSAS +REDRESSEES +REDRESSERAS +REDRESSEUSE +REDUCTASES +REDUIRAIS +REDUISAIENT +REDUISIONS +REDUISONS +REDUTES +REDYNAMISAS +REDYNAMISEE +REE +REECHELONNEZ +REECOUTANT +REECOUTEE +REECOUTERENT +REECOUTONS +REECRIRIONS +REECRIVAIENT +REECRIVIONS +REECRIVONS +REEDIFIEES +REEDIFIEREZ +REEDITA +REEDITASSES +REEDITERA +REEDITERONS +REEDUQUA +REEDUQUASSES +REEDUQUERA +REEDUQUERONS +REELIRE +REELISANT +REELLEMENT +REELUSSENT +REEMBAUCHAIT +REEMBAUCHE +REEMBAUCHONS +REEMETTEUR +REEMETTRAIT +REEMIS +REEMPLOI +REEMPLOYAIT +REEMPLOYATES +REEMPLOYONS +REEMPRUNTERA +REENCHANTAIT +REENCHANTE +REENCHANTONS +REENGAGEASSE +REENGAGERENT +REENREGISTRA +REENREGISTRE +REENTENDAIS +REENTENDRA +REENTENDRONS +REENVISAGEE +REEQUILIBRA +REEQUILIBREZ +REEQUIPANT +REEQUIPEE +REEQUIPERENT +REEQUIPONS +REERIEZ +REESCOMPTEE +REESSAIE +REESSAYAIT +REESSAYATES +REESSAYERAIT +REESSAYIEZ +REETUDIAS +REETUDIEES +REETUDIEREZ +REEVALUA +REEVALUASSES +REEVALUENT +REEVALUERIEZ +REEXAMENS +REEXAMINER +REEXECUTES +REEXPEDIAMES +REEXPEDIE +REEXPEDIERAS +REEXPEDIIONS +REEXPLIQUANT +REEXPLIQUEES +REEXPORTAI +REEXPORTER +REFACONNAI +REFACONNERAI +REFACTURES +REFAISANT +REFASSES +REFENDAIT +REFENDIS +REFENDRAI +REFENDRONT +REFERAIT +REFERATES +REFERENCANT +REFERENCEE +REFERENCIEZ +REFERENTIEL +REFERERAIT +REFERIEZ +REFERMANT +REFERMEE +REFERMENTAS +REFERMENTEE +REFERMER +REFERMERIONS +REFILA +REFILASSES +REFILERA +REFILERONS +REFINANCERA +REFISSENT +REFIXAIT +REFIXATES +REFIXERAIT +REFIXIEZ +REFLECHIS +REFLECHISSES +REFLECTIFS +REFLETA +REFLETASSES +REFLETERA +REFLETERONS +REFLEURIES +REFLEURIREZ +REFLEURISSE +REFLEX +REFLEXIONS +REFLEXOLOGIE +REFLUAS +REFLUER +REFLUERIONS +REFONDA +REFONDASSES +REFONDERAIT +REFONDIEZ +REFONDIT +REFONDREZ +REFONT +REFORMAI +REFORMASSIEZ +REFORMATAS +REFORMATEES +REFORMATEREZ +REFORMATION +REFORMERA +REFORMERONS +REFORMINGS +REFORMULAIS +REFORMULAT +REFORMULEZ +REFOUILLANT +REFOUILLEE +REFOUILLIEZ +REFOULAS +REFOULEES +REFOULERAS +REFOULEZ +REFOURGUAIT +REFOURGUATES +REFOURGUIEZ +REFOUTENT +REFOUTRAIT +REFOUTUES +REFRACTANT +REFRACTEE +REFRACTERENT +REFRACTIEZ +REFRANGIBLES +REFRENASSENT +REFRENEMENTS +REFRENEREZ +REFRIGERA +REFRIGERAS +REFRIGEREZ +REFROIDI +REFROIDIRAS +REFUGIAIT +REFUGIATES +REFUGIERAIT +REFUGIIEZ +REFUMAS +REFUMEES +REFUMEREZ +REFUS +REFUSAS +REFUSEES +REFUSEREZ +REFUTA +REFUTANT +REFUTATIONS +REFUTERAIT +REFUTIEZ +REGAGNAIT +REGAGNATES +REGAGNERAIT +REGAGNIEZ +REGALAGES +REGALASSES +REGALEMENT +REGALERENT +REGALIENNES +REGARDAI +REGARDASSE +REGARDENT +REGARDERIEZ +REGARDIEZ +REGARNIRAI +REGARNIRONT +REGARNISSIEZ +REGATAMES +REGATE +REGATEREZ +REGATIERS +REGAZONNANT +REGAZONNEE +REGAZONNONS +REGELASSE +REGELENT +REGELERIEZ +REGENCE +REGENERAMES +REGENERES +REGENTAIT +REGENTATES +REGENTERAIT +REGENTIEZ +REGIES +REGIMBASSENT +REGIMBEMENTS +REGIMBEREZ +REGIMBEZ +REGINGLARD +REGIONALISA +REGIONALISAT +REGIONS +REGIRIEZ +REGISSENT +REGISTRA +REGISTRASSE +REGISTREE +REGISTRERENT +REGISTRONS +REGLAIS +REGLAT +REGLEMENTER +REGLERA +REGLERONS +REGLEUSES +REGLOS +REGNANTE +REGNATES +REGNERENT +REGNIEZ +REGOMMAIS +REGOMMAT +REGOMMERAIS +REGOMMEZ +REGONFLAIT +REGONFLATES +REGONFLES +REGORGEAIT +REGORGEATES +REGORGERAIT +REGORGIEZ +REGRATTAIT +REGRATTATES +REGRATTERAIT +REGRATTIER +REGREAIS +REGREAT +REGREERAIS +REGREEZ +REGREFFER +REGREONS +REGRESSERAI +REGRESSERONT +REGRESSONS +REGRETTAIT +REGRETTATES +REGRETTERAIT +REGRETTIEZ +REGRIMPAMES +REGRIMPE +REGRIMPERAS +REGRIMPIONS +REGROSSIS +REGROUPANT +REGROUPEE +REGROUPERAIT +REGROUPIEZ +REGULAIT +REGULAT +REGULEES +REGULEREZ +REGULIERES +REGURGITAIT +REGURGITATES +REGURGITES +REHABILITAT +REHABILITES +REHABITUAMES +REHABITUE +REHABITUERAS +REHABITUIONS +REHAUSSANT +REHAUSSEE +REHAUSSERAIT +REHAUSSEURS +REHYDRATAI +REHYDRATER +REICHSTAG +REIFIAS +REIFIE +REIFIERAS +REIFIIONS +REIMPLANTEES +REIMPORTAI +REIMPORTER +REIMPOSAIENT +REIMPOSES +REIMPRIMAI +REIMPRIMERAI +REIMPUTAIT +REIMPUTATES +REIMPUTES +REINCARCEREE +REINCARNAI +REINCARNER +REINCORPORE +REINDEXA +REINDEXASSES +REINDEXERA +REINDEXERONS +REINETTES +REINFECTER +REINJECTAS +REINJECTEES +REINJECTEREZ +REINS +REINSCRIRAS +REINSCRITES +REINSCRIVIEZ +REINSERASSE +REINSERENT +REINSERERIEZ +REINSERTIONS +REINSTALLENT +REINSTAURAT +REINSTAURIEZ +REINTEGRAMES +REINTEGRAT +REINTEGRERAI +REINTRODUITE +REINVENTAS +REINVENTEES +REINVENTEREZ +REINVENTONS +REINVESTITES +REINVITER +REITERA +REITERAS +REITERATION +REITERERAI +REITERERONT +REJAILLIES +REJAILLIREZ +REJAILLISSE +REJECTION +REJETAMES +REJETE +REJETONNE +REJETTERAIT +REJOIGNAIT +REJOIGNIS +REJOINDRAI +REJOINDRONT +REJOINTOYA +REJOINTOYEUR +REJOUAI +REJOUASSIEZ +REJOUERAI +REJOUERONT +REJOUIRA +REJOUIRONS +REJOUITES +REJUGEAS +REJUGENT +REJUGEREZ +RELACAI +RELACASSIEZ +RELACERAI +RELACERONT +RELACHAS +RELACHEES +RELACHERAS +RELACHIONS +RELAIERAIS +RELAISSA +RELAISSASSES +RELAISSERA +RELAISSERONS +RELANCAIS +RELANCAT +RELANCERAIS +RELANCEUR +RELAPSES +RELARGIS +RELARGUANT +RELARGUEE +RELARGUERENT +RELARGUONS +RELATASSENT +RELATER +RELATERIONS +RELATIVISAI +RELATIVISERA +RELATIVITE +RELAVAMES +RELAVE +RELAVERAS +RELAVIONS +RELAXANTE +RELAXATES +RELAXERAIENT +RELAXES +RELAYAIS +RELAYAT +RELAYERAIS +RELAYEUR +RELECTRICE +RELEGUAIENT +RELEGUES +RELEVAGE +RELEVASSE +RELEVEMENT +RELEVERENT +RELEVEUSES +RELIAIS +RELIAT +RELIERAI +RELIERONT +RELIFTAIS +RELIFTAT +RELIFTERAIS +RELIFTEZ +RELIQUATS +RELIREZ +RELISE +RELIURE +RELOCALISEES +RELOGEA +RELOGEASSES +RELOGEONS +RELOGERIEZ +RELOOKAGES +RELOOKASSES +RELOOKERA +RELOOKERONS +RELOOKINGS +RELOQUETAS +RELOQUETEES +RELOQUETEREZ +RELOU +RELOUASSENT +RELOUER +RELOUERIONS +RELUCTANCE +RELUIRAIT +RELUISAIENT +RELUISEZ +RELUQUAIS +RELUQUAT +RELUQUERAIS +RELUQUEZ +RELUSSIONS +REMACHAIENT +REMACHES +REMAILLAIS +REMAILLAT +REMAILLERAIS +REMAILLEUR +REMANENCE +REMANGEAIS +REMANGEAT +REMANGERAIS +REMANGEZ +REMANIAMES +REMANIE +REMANIERAIS +REMANIEZ +REMAQUILLANT +REMAQUILLEES +REMARCHAI +REMARCHERAIS +REMARCHEZ +REMARIAIT +REMARIATES +REMARIERAIT +REMARIIEZ +REMARQUAIT +REMARQUATES +REMARQUERAIT +REMARQUIEZ +REMASTERISAS +REMASTERISER +REMASTIQUAT +REMASTIQUIEZ +REMBALLAMES +REMBALLE +REMBALLERAS +REMBALLIONS +REMBARQUASSE +REMBARQUONS +REMBARRER +REMBAUCHES +REMBLAIERA +REMBLAIERONT +REMBLAVAS +REMBLAVEES +REMBLAVEREZ +REMBLAYA +REMBLAYASSE +REMBLAYENT +REMBLAYERIEZ +REMBLAYONS +REMBOBINAS +REMBOBINEES +REMBOBINEREZ +REMBOITA +REMBOITASSE +REMBOITEMENT +REMBOITERENT +REMBOITONS +REMBOUGEASSE +REMBOUGEONS +REMBOUGERIEZ +REMBOURRAGES +REMBOURRERA +REMBOURRONS +REMBOURSAMES +REMBOURSE +REMBOURSEZ +REMBRAIERAIT +REMBRAYA +REMBRAYASSES +REMBRAYES +REMBRUNIRA +REMBRUNIRONS +REMBRUNISSES +REMBUCHAIS +REMBUCHAT +REMBUCHERAI +REMBUCHERONT +REMEDIABLE +REMEDIASSENT +REMEDIER +REMEDIERIONS +REMELAIENT +REMELASSIONS +REMELERAIENT +REMELES +REMEMBRAMES +REMEMBRE +REMEMBRERAIS +REMEMBREZ +REMEMORANT +REMEMORERAIT +REMEMORIEZ +REMERCIAS +REMERCIEES +REMERCIERAS +REMERCIIONS +REMESURANT +REMESUREE +REMESURERENT +REMESURONS +REMETTES +REMETTRAS +REMEUBLAIS +REMEUBLAT +REMEUBLERAIS +REMEUBLEZ +REMIMES +REMISAIS +REMISAT +REMISERAIS +REMISEZ +REMISSES +REMITTENT +REMIXAIS +REMIXAT +REMIXERAIS +REMIXEZ +REMMAILLAIS +REMMAILLAT +REMMAILLEUR +REMMAILLOTE +REMMANCHA +REMMANCHERA +REMMENAIS +REMMENAT +REMMENERAIS +REMMENEZ +REMMOULAIT +REMMOULATES +REMMOULERAIT +REMMOULIEZ +REMOBILISAIS +REMOBILISEZ +REMODELAIT +REMODELATES +REMODELERAIT +REMODELIEZ +REMONTAIENT +REMONTASSENT +REMONTER +REMONTERIONS +REMONTIONS +REMONTRANCE +REMONTRAT +REMONTRERAIS +REMONTREZ +REMORDANT +REMORDISSE +REMORDRAIENT +REMORDS +REMORQUAI +REMORQUERAI +REMORQUERONT +REMOTIVA +REMOTIVASSES +REMOTIVENT +REMOTIVERIEZ +REMOUDRA +REMOUDRONS +REMOUILLAMES +REMOUILLE +REMOUILLERAS +REMOUILLIONS +REMOULAIT +REMOULATES +REMOULERAIT +REMOULEURS +REMOULUMES +REMOUS +REMPAILLAS +REMPAILLEES +REMPAILLEREZ +REMPAILLEZ +REMPAQUETANT +REMPAQUETEES +REMPART +REMPIETASSE +REMPIETEMENT +REMPIETERENT +REMPIETONS +REMPILASSENT +REMPILER +REMPILERIONS +REMPLACABLES +REMPLACAS +REMPLACEES +REMPLACERAS +REMPLACIONS +REMPLIAMES +REMPLIE +REMPLIERAS +REMPLIIONS +REMPLIRENT +REMPLISSAIS +REMPLISSEZ +REMPLOIERAI +REMPLOIES +REMPLOYASSE +REMPLOYER +REMPLUMAIS +REMPLUMAT +REMPLUMERAIS +REMPLUMEZ +REMPOCHANT +REMPOCHEE +REMPOCHERENT +REMPOCHONS +REMPORTAMES +REMPORTE +REMPORTERAS +REMPORTIONS +REMPOTANT +REMPOTEE +REMPOTERENT +REMPOTONS +REMPRUNTER +REMUABLE +REMUANTE +REMUATES +REMUERAIENT +REMUES +REMUNERA +REMUNERASSES +REMUNERES +RENACLAIT +RENACLATES +RENACLERENT +RENACLONS +RENAISSANTS +RENAITRAI +RENAITRONT +RENAQUISSIEZ +RENARDAMES +RENARDE +RENARDERAS +RENARDIERES +RENAUDAIS +RENAUDAT +RENAUDERAS +RENAUDIONS +RENCAISSAMES +RENCAISSE +RENCAISSEZ +RENCARDAMES +RENCARDE +RENCARDERAS +RENCARDIONS +RENCHAINAMES +RENCHAINE +RENCHAINERAS +RENCHAINIONS +RENCHAUSSER +RENCHERIMES +RENCHERIRIEZ +RENCOGNAS +RENCOGNEES +RENCOGNEREZ +RENCONTRA +RENCONTRERA +RENDAIT +RENDIONS +RENDONS +RENDORMIE +RENDORMIRAIT +RENDOSSAI +RENDOSSERAI +RENDOSSERONT +RENDRAIT +RENDUE +RENEGOCIA +RENEGOCIASSE +RENEGOCIEE +RENEGOCIONS +RENETTAI +RENETTASSIEZ +RENETTERAI +RENETTERONT +RENFAITAIENT +RENFAITES +RENFERMAMES +RENFERME +RENFERMERAIS +RENFERMEZ +RENFILANT +RENFILEE +RENFILERENT +RENFILONS +RENFLAMMAIS +RENFLAMMAT +RENFLAMMEZ +RENFLASSIONS +RENFLERA +RENFLERONS +RENFLOUAI +RENFLOUER +RENFONCAIENT +RENFONCERA +RENFONCERONS +RENFORCAI +RENFORCEREZ +RENFORCIEZ +RENFORCIRENT +RENFORCONS +RENFORMIRAIT +RENFORMIT +RENFROGNANT +RENFROGNEE +RENFROGNIEZ +RENGAGEANT +RENGAGEES +RENGAGERAIT +RENGAGIEZ +RENGAINASSE +RENGAINENT +RENGAINERIEZ +RENGORGEA +RENGORGEONS +RENGORGERIEZ +RENGRAISSAT +RENGRAISSIEZ +RENGRENAS +RENGRENEES +RENGRENERAS +RENGRENIONS +RENIASSE +RENIEMENT +RENIERENT +RENIFLAIENT +RENIFLASSES +RENIFLENT +RENIFLERIEZ +RENIFLEUSES +RENINES +RENIPPASSE +RENIPPENT +RENIPPERIEZ +RENITENCES +RENOM +RENOMMASSENT +RENOMMER +RENOMMERIONS +RENONCA +RENONCAS +RENONCEES +RENONCERAS +RENONCULACEE +RENOTAMES +RENOTE +RENOTERAS +RENOTEUSE +RENOUAIT +RENOUATES +RENOUERAIENT +RENOUES +RENOUVELAI +RENOUVELASSE +RENOUVELER +RENOUVELLERA +RENOUVELLES +RENOVASSE +RENOVATIONS +RENOVERAIENT +RENOVES +RENQUILLAMES +RENQUILLE +RENQUILLERAS +RENQUILLIONS +RENSEIGNASSE +RENSEIGNONS +RENTABLES +RENTAMAMES +RENTAME +RENTAMERAS +RENTAMIONS +RENTATES +RENTERAIT +RENTIER +RENTOILAIENT +RENTOILES +RENTRA +RENTRAIERAIT +RENTRAIS +RENTRANTES +RENTRAYA +RENTRAYASSE +RENTRAYENT +RENTRAYERIEZ +RENTRAYIEZ +RENTRERAIENT +RENTRES +RENUMEROTEE +RENVERRA +RENVERRONT +RENVERSANTE +RENVERSATES +RENVERSES +RENVIDAIS +RENVIDAT +RENVIDERAIS +RENVIDEUR +RENVOIENT +RENVOYAS +RENVOYEES +RENVOYIONS +REOCCUPASSE +REOCCUPEE +REOCCUPERENT +REOCCUPONS +REOPERASSE +REOPERENT +REOPERERIEZ +REORCHESTRAI +REORDONNANT +REORDONNEE +REORDONNONS +REORGANISE +REORGANISONS +REORIENTEES +REORIENTEREZ +REOUVERT +REOUVRE +REOUVRIS +REOXYGENAI +REOXYGENERAI +REPAIERAIENT +REPAIRA +REPAIRASSES +REPAIRES +REPAISSE +REPAITRAIENT +REPAND +REPANDIMES +REPANDITES +REPANDRIEZ +REPARABLE +REPARAISSENT +REPARAITRAIS +REPARANT +REPARATEURS +REPARENT +REPARERIEZ +REPARLAIENT +REPARLES +REPARTAGEAI +REPARTAGES +REPARTEMENTS +REPARTIR +REPARTIRIONS +REPARTISSES +REPARTITIONS +REPARUSSE +REPASSA +REPASSASSE +REPASSENT +REPASSERIEZ +REPASSIEZ +REPAVAMES +REPAVE +REPAVERAIS +REPAVEZ +REPAYANT +REPAYEE +REPAYERENT +REPAYONS +REPECHAS +REPECHEES +REPECHEREZ +REPEIGNA +REPEIGNASSES +REPEIGNERA +REPEIGNERONS +REPEINDRAIS +REPEINT +REPENCHANT +REPENCHEE +REPENCHERENT +REPENCHONS +REPENDIEZ +REPENDIT +REPENDREZ +REPENS +REPENSASSENT +REPENSER +REPENSERIONS +REPENTAIS +REPENTES +REPENTIONS +REPENTIRIEZ +REPERAIENT +REPERASSIONS +REPERCAMES +REPERCE +REPERCERAS +REPERCIONS +REPERCUTAIT +REPERCUTATES +REPERCUTIEZ +REPERDES +REPERDISSIEZ +REPERDRAS +REPERDUES +REPERERAIS +REPEREZ +REPERTORIAIT +REPERTORIE +REPERTORIONS +REPESASSENT +REPESER +REPESERIONS +REPETAIENT +REPETASSIONS +REPETERAIENT +REPETES +REPETITION +REPETITRICES +REPETRIRAIS +REPETRISSONS +REPEUPLAS +REPEUPLEES +REPEUPLERAS +REPEUPLIONS +REPIPANT +REPIPEE +REPIPERENT +REPIPONS +REPIQUAS +REPIQUEES +REPIQUEREZ +REPIQUIONS +REPLACANT +REPLACEE +REPLACERAIT +REPLACIEZ +REPLANTAS +REPLANTE +REPLANTERAS +REPLANTIONS +REPLATRAMES +REPLATRE +REPLATRERAS +REPLATRIONS +REPLETIVE +REPLIA +REPLIASSE +REPLICATIONS +REPLIERA +REPLIERONS +REPLIQUAIS +REPLIQUAT +REPLIQUERAIS +REPLIQUEZ +REPLISSAMES +REPLISSE +REPLISSERAS +REPLISSIONS +REPLOIERAIT +REPLONGEAI +REPLONGERAI +REPLONGERONT +REPLOYAMES +REPLOYE +REPLU +REPOINTASSE +REPOINTENT +REPOINTERIEZ +REPOLIE +REPOLIRENT +REPOLISSAIS +REPOLIT +REPONDE +REPONDIONS +REPONDONS +REPONDRIONS +REPONSES +REPORTAIT +REPORTATES +REPORTERAIT +REPORTEUR +REPOSA +REPOSAS +REPOSEES +REPOSEREZ +REPOSITIONNE +REPOUDRAIS +REPOUDRAT +REPOUDRERAIS +REPOUDREZ +REPOURVOIT +REPOURVUE +REPOURVUT +REPOUSSANT +REPOUSSAT +REPOUSSERAI +REPOUSSERONT +REPRECISES +REPRENAIS +REPRENDRE +REPRENEUSES +REPRESENTES +REPRESSIFS +REPRIMAMES +REPRIMANDERA +REPRIMASSES +REPRIMERA +REPRIMERONS +REPRIS +REPRISAS +REPRISEES +REPRISEREZ +REPRISIONS +REPROBATEURS +REPROCHAIS +REPROCHAT +REPROCHERAIS +REPROCHEZ +REPRODUCTION +REPRODUIRAIT +REPRODUISAIS +REPRODUITE +REPROFILAMES +REPROFILE +REPROFILERAS +REPROFILIONS +REPROGRAMMES +REPROUVAS +REPROUVEES +REPROUVEREZ +REPS +REPUBLIA +REPUBLIASSES +REPUBLIE +REPUBLIERAS +REPUBLIIONS +REPUDIANT +REPUDIATIONS +REPUDIERAIT +REPUDIIEZ +REPUGNAMES +REPUGNASSES +REPUGNERA +REPUGNERONS +REPULSIONS +REPUSSIONS +REPUTASSE +REPUTEE +REPUTERENT +REPUTONS +REQUALIFIER +REQUERAIS +REQUERONS +REQUERRONS +REQUETASSE +REQUETENT +REQUETERIEZ +REQUIERE +REQUINQUAIS +REQUINQUAT +REQUINQUEZ +REQUISITION +REQUISSIONS +REQUITTAS +REQUITTEES +REQUITTEREZ +REROUTA +REROUTASSE +REROUTENT +REROUTERIEZ +RESALAI +RESALASSIEZ +RESALERAI +RESALERONT +RESALIRA +RESALIRONS +RESALISSEZ +RESARCELES +RESCAPASSENT +RESCAPER +RESCAPERIONS +RESCINDABLES +RESCINDAS +RESCINDEES +RESCINDEREZ +RESCISION +RESEAU +RESEAUTAS +RESEAUTEES +RESEAUTEREZ +RESEAUX +RESEMAIS +RESEMAT +RESEMERAIS +RESEMEZ +RESEQUANT +RESEQUEE +RESEQUERENT +RESEQUONS +RESERVAS +RESERVATION +RESERVERAIS +RESERVEZ +RESIDAIENT +RESIDAS +RESIDENCES +RESIDERA +RESIDERONS +RESIDUEL +RESIGNAIS +RESIGNAT +RESIGNER +RESIGNERIONS +RESILIABLES +RESILIASSES +RESILIENCE +RESILIERAIS +RESILIEZ +RESINAIT +RESINATES +RESINERAIT +RESINEUSES +RESINIFERE +RESISTAIENT +RESISTAS +RESISTER +RESISTERIONS +RESISTIONS +RESITUAIENT +RESITUES +RESOLUBLES +RESOLUSSIEZ +RESOLUTOIRE +RESOLVANTS +RESONANT +RESONNAIENT +RESONNAS +RESONNEES +RESONNERAS +RESONNIONS +RESORBANT +RESORBEE +RESORBERENT +RESORBONS +RESOUDRAIS +RESOUT +RESPECTAIS +RESPECTAT +RESPECTERAIS +RESPECTEZ +RESPECTUEUSE +RESPIRAIS +RESPIRASSES +RESPIRERAIT +RESPIRIEZ +RESQUILLES +RESSACS +RESSAIERIEZ +RESSAIGNAMES +RESSAIGNE +RESSAIGNERAS +RESSAIGNIONS +RESSAISIRAIS +RESSASSAIT +RESSASSATES +RESSASSERAIT +RESSASSEURS +RESSAUTA +RESSAUTASSES +RESSAUTERA +RESSAUTERONS +RESSAYAGES +RESSAYASSES +RESSAYERA +RESSAYERONS +RESSEMAIS +RESSEMAT +RESSEMBLANT +RESSEMBLAT +RESSEMBLERAS +RESSEMBLIONS +RESSEMELAIS +RESSEMELAT +RESSEMELIONS +RESSEMERAIS +RESSEMEZ +RESSENTE +RESSENTIMES +RESSENTIREZ +RESSERRAIT +RESSERRATES +RESSERRES +RESSERVANT +RESSERVIONS +RESSERVIRIEZ +RESSERVIT +RESSORTENT +RESSORTIRA +RESSORTIRONS +RESSORTISSE +RESSORTS +RESSOUDER +RESSOURCERA +RESSOUVENAIS +RESSOUVENUE +RESSOUVINSSE +RESSUAI +RESSUASSIEZ +RESSUERAI +RESSUERONT +RESSUIERAIT +RESSUIS +RESSURGIRAIS +RESSUSCITAS +RESSUSCITENT +RESSUYAGES +RESSUYASSES +RESSUYES +RESTAMES +RESTASSES +RESTAURAIT +RESTAURAT +RESTAUREES +RESTAUREREZ +RESTAUROUTE +RESTERAI +RESTERONT +RESTITUAIS +RESTITUAT +RESTITUERAIS +RESTITUEZ +RESTOROUTE +RESTREIGNEZ +RESTREIGNIT +RESTREINDREZ +RESTRICTIF +RESTRUCTURA +RESTRUCTURAT +RESTYLAMES +RESTYLE +RESTYLERAS +RESTYLIONS +RESULTANTS +RESUMAMES +RESUME +RESUMERAS +RESUMIONS +RESURGENT +RESURGIRAI +RESURGIRONT +RESURGISSIEZ +RETABLI +RETABLIRAS +RETABLISSAIT +RETABLISSONS +RETAILLAS +RETAILLEES +RETAILLEREZ +RETAIS +RETAMAMES +RETAME +RETAMERAS +RETAMEZ +RETAPAIT +RETAPATES +RETAPERAIT +RETAPIEZ +RETAPISSASSE +RETAPISSENT +RETARD +RETARDANTS +RETARDATES +RETARDENT +RETARDERIEZ +RETASSURE +RETATASSE +RETATENT +RETATERIEZ +RETAXAI +RETAXASSIEZ +RETAXERAI +RETAXERONT +RETEIGNE +RETEINDRAIS +RETEINT +RETELEPHONER +RETEND +RETENDIMES +RETENDITES +RETENDRE +RETENDUS +RETENTAIT +RETENTATES +RETENTERAIT +RETENTEURS +RETENTIRA +RETENTIRONS +RETENTISSE +RETENTITES +RETERCAGES +RETERCASSES +RETERCERA +RETERCERONS +RETERSAI +RETERSASSIEZ +RETERSERAI +RETERSERONT +RETICENT +RETICULAIT +RETICULATES +RETICULES +RETICULOSES +RETIENDRIEZ +RETIFS +RETIGEASSE +RETIGERA +RETIGERONS +RETINIEN +RETINOIQUES +RETINSSIEZ +RETIRAGES +RETIRASSE +RETIREE +RETIRERAIT +RETIRIEZ +RETISSAMES +RETISSE +RETISSERAS +RETISSIONS +RETOMBAIENT +RETOMBASSENT +RETOMBEMENTS +RETOMBEREZ +RETOND +RETONDIMES +RETONDITES +RETONDRIEZ +RETOQUAGE +RETOQUASSENT +RETOQUER +RETOQUERIONS +RETORDAGES +RETORDEUR +RETORDISSE +RETORDRA +RETORDRONS +RETORQUAI +RETORQUERAI +RETORQUERONT +RETORSIONS +RETOUCHAMES +RETOUCHE +RETOUCHERAS +RETOUCHEUSE +RETOUPAIT +RETOUPATES +RETOUPERAIT +RETOUPIEZ +RETOURNAIT +RETOURNATES +RETOURNES +RETRACAIT +RETRACATES +RETRACERAIT +RETRACIEZ +RETRACTAIS +RETRACTAT +RETRACTERAI +RETRACTERONT +RETRACTIF +RETRACTONS +RETRADUIREZ +RETRADUISE +RETRAIE +RETRAIREZ +RETRAITAIS +RETRAITASSES +RETRAITENT +RETRAITERIEZ +RETRANCHA +RETRANCHENT +RETRANSCRIRA +RETRANSMISSE +RETRAVAILLE +RETRAVERSA +RETRAVERSES +RETRAYANTES +RETRECI +RETRECIRAS +RETRECISSAIT +RETRECISSONS +RETREIGNEZ +RETREINDRE +RETREINTS +RETREMPER +RETRIBUAIENT +RETRIBUES +RETROACTES +RETROAGI +RETROAGIREZ +RETROAGISSE +RETROCEDAI +RETROCEDERAI +RETROFLECHI +RETROGRADA +RETROGRADER +RETROUSSAIS +RETROUSSAT +RETROUSSERAI +RETROUVAI +RETROUVER +RETROVIRAL +RETUBA +RETUBASSES +RETUBERA +RETUBERONS +REUILLY +REUNIFIAMES +REUNIFIERAIS +REUNIFIEZ +REUNIRAS +REUNISSAIENT +REUNISSIEZ +REUSSIRA +REUSSIRONS +REUSSISSEZ +REUTILISAI +REUTILISER +REVACCINAI +REVACCINER +REVAILLE +REVALEZ +REVALUSSES +REVANCHAIT +REVANCHASSES +REVANCHERA +REVANCHERONS +REVAS +REVASSAIENT +REVASSERAIT +REVASSES +REVATES +REVAUDRONS +REVECUMES +REVEE +REVEILLAS +REVEILLEES +REVEILLEREZ +REVEILLEZ +REVELAIENT +REVELASSIONS +REVELEE +REVELERENT +REVELONS +REVENDAIS +REVENDEZ +REVENDIQUANT +REVENDIQUEES +REVENDIS +REVENDRAI +REVENDRONT +REVENONS +REVERAI +REVERASSIEZ +REVERBERANT +REVERBERAT +REVERBERERAI +REVERCHAIT +REVERCHATES +REVERCHERAIT +REVERCHIEZ +REVERDIRAI +REVERDIRONT +REVERDOIRS +REVERER +REVERERIONS +REVERIFIES +REVERNIR +REVERNIRIONS +REVERNISSE +REVERONT +REVERRONS +REVERSALES +REVERSATES +REVERSERAI +REVERSERONT +REVERSIONS +REVETAIS +REVETIMES +REVETIREZ +REVETISSIONS +REVEUILLENT +REVEUT +REVIENDRIEZ +REVIF +REVIGORANTES +REVIGORATION +REVIGORERAIS +REVIGOREZ +REVINSSES +REVIRAIENT +REVIRASSIONS +REVIRERA +REVIRERONS +REVISABLES +REVISASSES +REVISERA +REVISERONS +REVISIONNEL +REVISITAI +REVISITERAI +REVISITERONT +REVISSAI +REVISSASSIEZ +REVISSERAI +REVISSERONT +REVITALISAIS +REVITALISER +REVIVAIT +REVIVIFIAI +REVIVIFIER +REVIVRAIT +REVOCABLE +REVOILA +REVOLANT +REVOLEE +REVOLERENT +REVOLONS +REVOLTANTS +REVOLTEE +REVOLTERENT +REVOLTONS +REVOLVERISAS +REVOLVERISES +REVOQUAIENT +REVOQUES +REVOTAMES +REVOTE +REVOTERAS +REVOTIONS +REVOUDRIONS +REVOULOIR +REVOULUSSES +REVOYIEZ +REVULSAI +REVULSASSE +REVULSENT +REVULSERIEZ +REVULSIONS +REWRITAMES +REWRITE +REWRITERAS +REWRITEURS +REWRITRICES +RHABILLAMES +RHABILLE +RHABILLERAIS +RHABILLEUR +RHAPSODE +RHEIFORMES +RHEOBASES +RHEOPHILES +RHESUS +RHETIQUE +RHINGRAVIAT +RHINOLOPHE +RHIZOBIUMS +RHIZOPHORE +RHIZOTOMES +RHODESIEN +RHODIAIT +RHODIATES +RHODIERA +RHODIERONS +RHODITES +RHODOPHYCEES +RHOMBOEDRES +RHONALPINE +RHUBARBES +RHUMASSE +RHUMATISANTS +RHUMATOLOGIE +RHUMBS +RHUMERAIT +RHUMES +RHYNCHONELLE +RHYTINE +RIAIT +RIBAT +RIBESIEE +RIBLANT +RIBLEE +RIBLERENT +RIBLON +RIBOSE +RIBOT +RIBOULAMES +RIBOULES +RICAINES +RICANANTES +RICANE +RICANERAS +RICANEUSE +RICERCARE +RICHELIEUX +RICHISSIMES +RICOCHAS +RICOCHER +RICOCHERIONS +RICOTTA +RIDAMES +RIDE +RIDER +RIDERIONS +RIDICULISAT +RIDICULISES +RIDULE +RIENS +RIF +RIFFLE +RIFLAMES +RIFLAT +RIFLERAIS +RIFLETTE +RIFTS +RIGIDIFIES +RIGOISE +RIGOLAIT +RIGOLASSES +RIGOLERAIENT +RIGOLES +RIGOLO +RIGOTTES +RIKIKIS +RIMAILLA +RIMAILLASSES +RIMAILLERA +RIMAILLERONS +RIMAILLONS +RIMASSIONS +RIMEE +RIMERENT +RIMEUSES +RINCA +RINCASSE +RINCEE +RINCERENT +RINCEURS +RING +RINGARDANT +RINGARDEE +RINGARDERENT +RINGARDISA +RINGARDISER +RINGGITS +RIOTAI +RIOTASSIEZ +RIOTERAIS +RIOTEZ +RIPAIENT +RIPAILLERAI +RIPAILLERONT +RIPAIS +RIPAT +RIPER +RIPERIONS +RIPIENOS +RIPOLINAIT +RIPOLINATES +RIPOLINERAIT +RIPOLINIEZ +RIPOSTAMES +RIPOSTE +RIPOSTERAS +RIPOSTIONS +RIPUAIRES +RIRENT +RISBERME +RISIBLE +RISQUAIT +RISQUATES +RISQUERAIT +RISQUIEZ +RISSOLAI +RISSOLASSIEZ +RISSOLERAI +RISSOLERONT +RISTOURNAIT +RISTOURNATES +RISTOURNIEZ +RITALES +RITUALISERA +RITUALISTES +RIVAIENT +RIVALISAIT +RIVALISATES +RIVALISERENT +RIVALISONS +RIVASSIONS +RIVER +RIVERAIS +RIVESALTES +RIVETANT +RIVETEE +RIVETONS +RIVETTEREZ +RIVEZ +RIVURE +RIYAL +RIZICULTEURS +ROADSTERS +ROBAIS +ROBAT +ROBERAI +ROBERONT +ROBIN +ROBINSONS +ROBOTISANT +ROBOTISERAIT +ROBOTISIEZ +ROBUSTAS +ROCAILLAGES +ROCAMBOLE +ROCHAGES +ROCHASSES +ROCHEE +ROCHERAIS +ROCHES +ROCHIONS +ROCKEURS +ROCOUAI +ROCOUASSIEZ +ROCOUERAI +ROCOUERONT +RODA +RODAILLAMES +RODAILLE +RODAILLEREZ +RODAIS +RODAT +RODERAI +RODERONT +RODOIRS +ROENTGENIUM +ROGATONS +ROGNAS +ROGNEES +ROGNEREZ +ROGNEUX +ROGNONNAI +ROGNONNERAIS +ROGNONNEZ +ROGUEE +ROIDI +ROIDIRAS +ROIDISSAIT +ROIDITES +ROILLASSENT +ROILLER +ROILLERIONS +ROITELETS +ROLLER +ROLLOT +ROMAINES +ROMANCAMES +ROMANCE +ROMANCERAS +ROMANCEZ +ROMAND +ROMANESQUE +ROMANISAIENT +ROMANISEES +ROMANISEREZ +ROMANISMES +ROMANTIQUES +ROMANTISER +ROMARIN +ROMPAIT +ROMPIS +ROMPRAI +ROMPRONT +ROMSTECK +RONCHONNA +RONCHONNENT +RONCHONNIEZ +RONCIERES +RONDEL +RONDEUR +RONDIR +RONDIRIONS +RONDISSES +RONEOTAIT +RONEOTATES +RONEOTERAIT +RONEOTIEZ +RONEOTYPAS +RONEOTYPEES +RONEOTYPEREZ +RONERAIE +RONFLANTES +RONFLE +RONFLERAS +RONFLEUR +RONGEAI +RONGEASSE +RONGEMENTS +RONGERENT +RONGEUSES +RONRONNAI +RONRONNERAI +RONRONNERONT +RONSARDISAIS +RONTGEN +ROOKERY +ROQUANT +ROQUEFORT +ROQUERAIENT +ROQUERONS +ROQUIEZ +ROSA +ROSAIRES +ROSANILINE +ROSATES +ROSELET +ROSEOLES +ROSERENT +ROSEURS +ROSIER +ROSIMES +ROSIREZ +ROSISSE +ROSITES +ROSSARDE +ROSSATES +ROSSERAIT +ROSSES +ROSTI +ROTACE +ROTAMES +ROTARY +ROTATEUR +ROTATIVE +ROTENGLE +ROTERAIT +ROTEURS +ROTIMES +ROTIRAS +ROTISSAIENT +ROTISSEURS +ROTITES +ROTOR +ROTS +ROTULIENS +ROUAGE +ROUANNETTE +ROUAT +ROUBLA +ROUBLARDISE +ROUBLATES +ROUBLERAIT +ROUBLIEZ +ROUCOULAIS +ROUCOULASSES +ROUCOULENT +ROUCOULERIEZ +ROUDOUDOU +ROUENNERIE +ROUERAIENT +ROUERIE +ROUETTES +ROUGE +ROUGEOIENT +ROUGEOIERONS +ROUGEOYA +ROUGEOYAS +ROUGEOYERENT +ROUGEUR +ROUGIRAI +ROUGIRONT +ROUGISSEMENT +ROUI +ROUILLANT +ROUILLEE +ROUILLERENT +ROUILLIEZ +ROUIRAIENT +ROUIS +ROUISSEZ +ROULADES +ROULANTES +ROULE +ROULEMENTS +ROULEREZ +ROULEUSE +ROULOIR +ROULOTTAMES +ROULOTTE +ROULOTTERAS +ROULOTTIERE +ROUMAINES +ROUMEGUANT +ROUMEGUENT +ROUMEGUERIEZ +ROUMIE +ROUPILLA +ROUPILLASSES +ROUPILLES +ROUQUETTE +ROUSCAILLAIT +ROUSCAILLE +ROUSPETAI +ROUSPETERAI +ROUSPETERONT +ROUSQUILLE +ROUSSEAUX +ROUSSELERENT +ROUSSELLERAI +ROUSSELLES +ROUSSEURS +ROUSSIRA +ROUSSIRONS +ROUSSISSENT +ROUSTE +ROUSTIRAIS +ROUSTISSONS +ROUTAIS +ROUTASSENT +ROUTER +ROUTERIONS +ROUTIERE +ROUTINIERES +ROUVERTES +ROUVRENT +ROUVRIRAIS +ROUVRISSE +ROYALISTE +ROYAUMAS +ROYAUMEES +ROYAUMEREZ +ROYAUTE +RUAIT +RUASSIEZ +RUBANAMES +RUBANE +RUBANERAS +RUBANEUR +RUBANIONS +RUBEFIAIS +RUBEFIASSES +RUBEFIERA +RUBEFIERONS +RUBENIENNE +RUBESCENT +RUBICOND +RUBIGINEUX +RUBRIQUAIT +RUBRIQUATES +RUBRIQUERAIT +RUBRIQUIEZ +RUCHAS +RUCHEES +RUCHEREZ +RUCHONS +RUDENTAI +RUDENTASSIEZ +RUDENTERAI +RUDENTERONT +RUDERALES +RUDIMENTS +RUDOIERAIS +RUDOLOGIES +RUDOYASSENT +RUDOYERENT +RUEILLOISE +RUERAIT +RUFFIAN +RUFIYAAS +RUGIES +RUGINASSE +RUGINEE +RUGINERENT +RUGINONS +RUGIRIEZ +RUGISSANTES +RUGISSONS +RUILAI +RUILASSIEZ +RUILERAI +RUILERONT +RUINAIT +RUINATES +RUINERAIT +RUINEUSEMENT +RUINONS +RUISSELAIT +RUISSELIEZ +RUMBA +RUMINAIENT +RUMINASSENT +RUMINEES +RUMINEREZ +RUMSTEAK +RUOLZ +RUPASSE +RUPELIEN +RUPERAIT +RUPESTRES +RUPINAIENT +RUPINASSIONS +RUPINERAIENT +RUPINES +RUPTURE +RURALITES +RUSAI +RUSASSIEZ +RUSERAI +RUSERONT +RUSSES +RUSSIFIEES +RUSSIFIEREZ +RUSSISA +RUSSISASSES +RUSSISERA +RUSSISERONS +RUSSOPHOBES +RUSTAUDS +RUSTIQUAIS +RUSTIQUAT +RUSTIQUERAIS +RUSTIQUEZ +RUTACEES +RUTILA +RUTILANTES +RUTILE +RUTILERAS +RUTILIONS +RWANDAISES +RYTHMAI +RYTHMASSIEZ +RYTHMERAI +RYTHMERONT +RYTHMIONS +SABAYON +SABBATIQUES +SABIR +SABLANT +SABLEE +SABLERENT +SABLEURS +SABLINE +SABLONNANT +SABLONNEE +SABLONNERENT +SABLONNEZ +SABORDA +SABORDASSE +SABORDEMENT +SABORDERENT +SABORDONS +SABOTAMES +SABOTE +SABOTERAS +SABOTEUR +SABOTIONS +SABOULAS +SABOULEES +SABOULEREZ +SABRA +SABRASSE +SABRENT +SABRERIEZ +SABREUSES +SABURRES +SACCADASSE +SACCADENT +SACCADERIEZ +SACCAGEA +SACCAGEASSES +SACCAGEONS +SACCAGERIEZ +SACCAGIEZ +SACCHARIDES +SACCHAROIDES +SACCULE +SACERDOTALES +SACHERIES +SACOLEVA +SACQUAIT +SACQUATES +SACQUERAIENT +SACQUES +SACRAL +SACRALISAS +SACRALISE +SACRALISERAS +SACRALISIONS +SACRASSES +SACREES +SACRERAS +SACREZ +SACRIFIASSE +SACRIFIEE +SACRIFIERENT +SACRIFIONS +SACRISTI +SADDUCEENNE +SADIQUEMENT +SADOMASOS +SAFOUTIER +SAFRANAS +SAFRANEES +SAFRANEREZ +SAFRANINE +SAGACITE +SAGESSE +SAGITTAL +SAGOUINE +SAGUENEEN +SAHEL +SAHRAOUIES +SAIETTAMES +SAIETTE +SAIETTERAS +SAIETTIONS +SAIGNANT +SAIGNAT +SAIGNERAI +SAIGNERONT +SAIGNOIR +SAILLENT +SAILLIRAIENT +SAILLIT +SAINFOINS +SAISINES +SAISIRIEZ +SAISISSANT +SAISISSIEZ +SAISONNIERES +SAKI +SALACE +SALADEROS +SALAGE +SALAISONNIER +SALAMANDRES +SALANTS +SALARIANT +SALARIAUX +SALARIERAIT +SALARIIEZ +SALATES +SALE +SALERA +SALERON +SALETES +SALICAIRES +SALICOQUES +SALICULTURES +SALIFERE +SALIFIANT +SALIFIERAIT +SALIFIIEZ +SALIN +SALINIERS +SALIRA +SALIRONS +SALISSE +SALIT +SALIVANT +SALIVAT +SALIVERAIS +SALIVEZ +SALMONELLA +SALOLS +SALONARDS +SALONNIERE +SALOPAIS +SALOPASSENT +SALOPERA +SALOPERIEZ +SALOPIAUX +SALPETRAGE +SALPETRER +SALPETRERIES +SALPETRIER +SALPICONS +SALSAS +SALTARELLE +SALTIQUE +SALUANT +SALUBRES +SALUERAIENT +SALUES +SALUTAIRES +SALVAGNIN +SAMARE +SAMBO +SAMEDIS +SAMMIES +SAMOLES +SAMOYEDE +SAMPLAIS +SAMPLAT +SAMPLERAIS +SAMPLES +SAMPOTS +SANATORIALE +SANCTIFIAIS +SANCTIFIE +SANCTIFIERAS +SANCTIFIIONS +SANCTIONNEE +SANCTIONS +SANCTUARISAS +SANCTUARISER +SANDALETTES +SANDINISMES +SANDWICH +SANDWICHER +SANDWICHS +SANGLAIS +SANGLASSES +SANGLERA +SANGLERONS +SANGLOT +SANGLOTER +SANGOS +SANGUIN +SANGUISORBE +SANIEUSE +SANQUETTE +SANSCRITISTE +SANSKRITISME +SANTALINE +SANTOLINE +SANTONIENNE +SANTOUR +SAOUDIEN +SAOULAIS +SAOULARDS +SAOULEE +SAOULERENT +SAOULIEZ +SAPAJOUS +SAPATES +SAPEQUES +SAPERENT +SAPEURS +SAPIDE +SAPIENTIEL +SAPINE +SAPITEUR +SAPONE +SAPONIFIANT +SAPONIFIIEZ +SAPOTE +SAPRISTI +SAPROPHILES +SAQUAIS +SAQUAT +SAQUER +SAQUERIONS +SARABANDES +SARBACANES +SARCELLOISES +SARCLAMES +SARCLE +SARCLERAS +SARCLEUR +SARCLURE +SARCOMERE +SARCOPLASMES +SARDE +SARDINIERES +SARGUE +SARLADAIS +SARMENTAIT +SARMENTATES +SARMENTERAIT +SARMENTEUSES +SARODISTE +SAROUEL +SARRASIN +SARRETES +SARS +SAS +SASSAIS +SASSASSIEZ +SASSENAGES +SASSEREZ +SASSEZ +SATANIQUES +SATELLISENT +SATINA +SATINASSE +SATINENT +SATINERIEZ +SATINEUSES +SATIRIQUES +SATIRISER +SATIS +SATISFAISANT +SATISFAITS +SATISFERAIS +SATISFIABLE +SATISFIT +SATRAPIQUES +SATURAMES +SATURASSIONS +SATURENT +SATURERIEZ +SATURNE +SATURNINES +SATYRIQUE +SAUCANT +SAUCEE +SAUCERENT +SAUCIER +SAUCISSON +SAUCISSONNAS +SAUCISSONNES +SAUGRENUE +SAUMATRES +SAUMONEUSE +SAUMURAIS +SAUMURAT +SAUMURERAIS +SAUMUREZ +SAUNAGES +SAUNASSE +SAUNERA +SAUNERONS +SAUNONS +SAUPOUDRAIS +SAUPOUDRAT +SAUPOUDREUR +SAUR +SAURAS +SAUREES +SAURERAS +SAUREZ +SAURIONS +SAURIRIEZ +SAURISSAIS +SAURISSEUSE +SAUROPHIDIEN +SAUSSURIENNE +SAUTAIT +SAUTATES +SAUTERAIENT +SAUTERIAU +SAUTEUR +SAUTILLAGES +SAUTILLAS +SAUTILLEREZ +SAUTIONS +SAUVAGEONNES +SAUVAGINS +SAUVASSES +SAUVEGARDAT +SAUVEGARDIEZ +SAUVERAS +SAUVETE +SAUVETTE +SAUVONS +SAVATAI +SAVATASSIEZ +SAVATERAI +SAVATERONT +SAVETIERS +SAVOISIENNES +SAVONNAMES +SAVONNE +SAVONNERAS +SAVONNETTE +SAVONNIERES +SAVOURAIT +SAVOURATES +SAVOURERAIT +SAVOYARDS +SAXIFRAGACEE +SAXONNE +SAYON +SCABINALES +SCALANTE +SCALDIQUES +SCALPAIT +SCALPATES +SCALPERAIENT +SCALPES +SCANDAI +SCANDALISA +SCANDALISES +SCANDASSES +SCANDERA +SCANDERONS +SCANDIUM +SCANNAIT +SCANNATES +SCANNERAIT +SCANNERISAIT +SCANNERISIEZ +SCANNIEZ +SCANSION +SCAPHOPODE +SCARIEUSES +SCARIFIANT +SCARIFIES +SCAROLE +SCELERAT +SCELLAIENT +SCELLASSIONS +SCELLERA +SCELLERONS +SCENARIO +SCENARISASSE +SCENARISEE +SCENARISONS +SCENOGRAPHE +SCEPTICISMES +SCHADAIENT +SCHADASSIONS +SCHADERAIT +SCHADIEZ +SCHAPPE +SCHEIDAIT +SCHEIDATES +SCHEIDERAIT +SCHEIDEURS +SCHELEMS +SCHELINGUEZ +SCHEMATIQUES +SCHEMATISENT +SCHEME +SCHIEDAM +SCHINDANT +SCHINDENT +SCHINDERIEZ +SCHIPPERKES +SCHISTOIDE +SCHIZOGAMIES +SCHIZOSE +SCHLAMM +SCHLINGUAIS +SCHLINGUAT +SCHLINGUERAS +SCHLINGUIONS +SCHLITTANT +SCHLITTEE +SCHLITTERENT +SCHLITTIEZ +SCHNOCK +SCHNOUFFS +SCHORRE +SCHWYZOISE +SCIAIT +SCIAS +SCIATIQUE +SCIENIDE +SCIENTISTES +SCIERAIT +SCIES +SCINCIDE +SCINDANT +SCINDEE +SCINDERENT +SCINDONS +SCINTILLE +SCINTILLERAS +SCINTILLIONS +SCIOTTES +SCISSIONNAIT +SCISSIONNE +SCISSURE +SCLERALES +SCLEREUX +SCLEROSANTE +SCLEROSATES +SCLEROSERAIT +SCLEROSIEZ +SCOLAIREMENT +SCOLARISANT +SCOLARISIEZ +SCOLIASTE +SCOLOPENDRE +SCONSE +SCOPE +SCORAIENT +SCORASSIONS +SCORENT +SCORERIEZ +SCORIACE +SCORIFIAIS +SCORIFIAT +SCORIFIERAIS +SCORIFIEZ +SCORPIOIDE +SCOTCH +SCOTCHASSENT +SCOTCHER +SCOTCHERIONS +SCOTIES +SCOTOMISAIS +SCOTOMISAT +SCOTOMISERAI +SCOTTISH +SCOUTES +SCRABBLANT +SCRABBLENT +SCRABBLERIEZ +SCRABBLEUSES +SCRAPBOOKS +SCRATCHAIENT +SCRATCHES +SCRIBANNES +SCRIBOUILLAT +SCRIPT +SCRIPTORIUMS +SCROFULAIRE +SCRUBS +SCRUTAIS +SCRUTAT +SCRUTEES +SCRUTEREZ +SCRUTIONS +SCULPTAIT +SCULPTATES +SCULPTERAIT +SCULPTEURE +SCULPTRICES +SCUTIFORME +SCYPHOZOAIRE +SEANCES +SEBACES +SEBKRAS +SECAM +SECESSION +SECHAIENT +SECHASSENT +SECHENT +SECHERESSES +SECHEURS +SECONDA +SECONDANTE +SECONDERAI +SECONDERONT +SECOUAGE +SECOUASSENT +SECOUEMENTS +SECOUEREZ +SECOUEZ +SECOURE +SECOURIR +SECOURRAIT +SECOURUE +SECOURUT +SECRETAIRE +SECRETAS +SECRETEES +SECRETERENT +SECRETEUSES +SECRETRICE +SECTATRICE +SECTIONNAIRE +SECTIONNER +SECTIONS +SECTORISAIT +SECTORISATES +SECTORISES +SECULARISAI +SECULARISERA +SECURISANT +SECURISAT +SECURISERAI +SECURISERONT +SECURITES +SEDATIVES +SEDIMENTA +SEDIMENTASSE +SEDIMENTEE +SEDITION +SEDUIRA +SEDUIRONS +SEDUISE +SEDUISISSENT +SEDUITS +SEFARDI +SEGATIERES +SEGMENTAIS +SEGMENTASSES +SEGMENTEES +SEGMENTEREZ +SEGMENTS +SEGREGATIF +SEGREGEAIS +SEGREGEAT +SEGREGERAIS +SEGREGEZ +SEGREGUAS +SEGREGUEES +SEGREGUEREZ +SEGUEDILLE +SEIDE +SEIGNEURIAUX +SEIN +SEISMAL +SEISMOGRAMME +SEIZIEMEMENT +SEJOURNAMES +SEJOURNE +SEJOURNEREZ +SEJOURS +SELANDIENNES +SELECTAS +SELECTEES +SELECTEREZ +SELECTIF +SELECTRICE +SELENIATES +SELENIEUSES +SELENIUMS +SELENOLOGUE +SELLAIENT +SELLASSIONS +SELLERAIENT +SELLERONS +SELLONS +SEMAIENT +SEMAIT +SEMAS +SEMAT +SEMBLAMES +SEMBLATES +SEMBLERENT +SEMBLONS +SEMELAGE +SEMENCIERES +SEMERENT +SEMESTRIELLE +SEMILLANTE +SEMINARISTE +SEMIOLOGIES +SEMITIQUES +SEMOIRS +SEMONCASSENT +SEMONCER +SEMONCERIONS +SEMOULERIE +SEN +SENATORIAL +SENE +SENEGALI +SENEGALISEES +SENEGALISONS +SENESCENT +SENESTRORSUM +SENILITES +SENNES +SENONIEN +SENSAS +SENSIBILISAI +SENSIBILISER +SENSIBLERIE +SENSITIVITES +SENSORIELLES +SENSUALISMES +SENT +SENTENCIEUX +SENTIES +SENTIMENTAUX +SENTIRAI +SENTIRONT +SEOIR +SEOULIENS +SEPARABLES +SEPARASSES +SEPARATIONS +SEPAREE +SEPARERAS +SEPARIONS +SEPPUKU +SEPTANTAINE +SEPTEMBRE +SEPTENNALE +SEPTILIENNE +SEPTOLET +SEPTUOR +SEPTUPLASSE +SEPTUPLENT +SEPTUPLERIEZ +SEPULCRALE +SEQUANE +SEQUENCAGES +SEQUENCASSES +SEQUENCERA +SEQUENCERONS +SEQUENTIELLE +SEQUESTRAS +SEQUESTRE +SEQUESTRERAS +SEQUESTRIONS +SERAIENT +SERANCAIENT +SERANCES +SERANCOLIN +SERAPHIQUE +SEREIN +SERENISSIMES +SEREUSE +SERFOUIE +SERFOUIRENT +SERFOUISSAIS +SERFOUIT +SERGETTES +SERIALITES +SERIAT +SERIE +SERIERAI +SERIERONT +SERIGRAPHIEE +SERINANT +SERINEE +SERINERENT +SERINGA +SERINGUAIT +SERINGUATES +SERINGUERONT +SERINS +SERMON +SERMONNAS +SERMONNEES +SERMONNEREZ +SERMONNEZ +SEROGROUPES +SEROTONINES +SERPENTAIRES +SERPENTERA +SERPENTERONS +SERPENTINS +SERPILLERE +SERRAGES +SERRANT +SERRAT +SERRERAIENT +SERRES +SERRONS +SERT +SERTIRA +SERTIRONS +SERTISSENT +SERTISSURE +SERVAIENT +SERVENT +SERVILE +SERVIRAIENT +SERVIS +SERVITEURS +SESAMIES +SESQUIOXYDES +SETACE +SETIFIEN +SETTER +SEULET +SEVERITE +SEVILLANES +SEVIRENT +SEVISSANT +SEVRA +SEVRASSE +SEVRENT +SEVRERIEZ +SEVRONS +SEXAGESIMAUX +SEXISMES +SEXONOMIES +SEXTES +SEXTINES +SEXTUPLAIENT +SEXTUPLES +SEXUALISAMES +SEXUALISEZ +SEXUEES +SEYANT +SEZIGUE +SHABBAT +SHAHNAIS +SHAKOS +SHAMISENS +SHAMPOOINAS +SHAMPOOINENT +SHAMPOOINGS +SHAMPOUINANT +SHAMPOUINEES +SHAMPOUINIEZ +SHANTUNGS +SHAVINGS +SHELF +SHERIFS +SHIDO +SHIKHARA +SHINTO +SHISHA +SHOGIS +SHOGUNAL +SHOOTA +SHOOTASSES +SHOOTERA +SHOOTERONS +SHORT +SHOWBIZ +SHRAPNELS +SHUNTAIENT +SHUNTASSIONS +SHUNTERAIENT +SHUNTES +SIALIQUE +SIAMOISES +SIBILANTS +SICAV +SICILIENNE +SICLAS +SICLEES +SICLEREZ +SICULE +SIDENOLOGIE +SIDERAMES +SIDERASSIONS +SIDERER +SIDERERIONS +SIDEROPHILE +SIDERURGIE +SIDOLOGUE +SIEGEAIENT +SIEGEASSIONS +SIEGERAIT +SIEGIEZ +SIERA +SIESTAIT +SIESTATES +SIESTERENT +SIESTONS +SIFFLAIS +SIFFLASSES +SIFFLENT +SIFFLERIEZ +SIFFLEUSES +SIFFLOTAIT +SIFFLOTATES +SIFFLOTES +SIGILLEE +SIGLEE +SIGNAI +SIGNALAMES +SIGNALE +SIGNALERAIS +SIGNALETIQUE +SIGNALISAI +SIGNALISER +SIGNANT +SIGNATAIRE +SIGNER +SIGNERIONS +SIGNIFIAIENT +SIGNIFIAS +SIGNIFIERAI +SIGNIFIERONT +SIKASSOIS +SIKHS +SILANT +SILENCE +SILENT +SILERENT +SILESIENS +SILHOUETTEE +SILICAGEL +SILICEUX +SILICONAI +SILICONERAI +SILICONERONT +SILICOTIQUE +SILLAGE +SILLONNAIS +SILLONNAT +SILLONNERAIS +SILLONNEZ +SILPHE +SILURES +SILVANERS +SIMARUBACEE +SIMIESQUE +SIMILICUIRS +SIMILISAIT +SIMILISATES +SIMILISERAIT +SIMILISIEZ +SIMMENTALS +SIMPLET +SIMPLIFIABLE +SIMPLIFIIEZ +SIMULACRE +SIMULASSENT +SIMULATRICE +SIMULERAIS +SIMULEZ +SINAPISAI +SINAPISERAI +SINAPISERONT +SINCERES +SINDHIE +SINGALETTES +SINGEAIT +SINGEATES +SINGERAIT +SINGES +SINGLETTE +SINGULETS +SINISAIENT +SINISASSENT +SINISEES +SINISEREZ +SINISTRALITE +SINITES +SINOPHILE +SINOQUE +SINTERISAS +SINTERISE +SINTERISERAS +SINTERISIONS +SINUAS +SINUEES +SINUEREZ +SINUIEZ +SINUSIEN +SINUSOIDE +SIPHOMYCETES +SIPHONNAIT +SIPHONNATES +SIPHONNES +SIPO +SIRES +SIROPERIES +SIROTASSE +SIROTENT +SIROTERIEZ +SIROTIEZ +SIRVENTES +SISMICITE +SISMOGRAPHIE +SISMOMETRIE +SISTRE +SITE +SITOSTEROL +SITUAI +SITUASSIEZ +SITUERAI +SITUERONT +SIVAITE +SIXTE +SKA +SKATEURS +SKETCH +SKIAIT +SKIASSIONS +SKIERAIS +SKIEUR +SKIFF +SKIMMIAS +SKIPPAIS +SKIPPAT +SKIPPERAIS +SKIPPES +SKIS +SKYSURF +SLALOMAIS +SLALOMAT +SLALOMERAS +SLALOMEUSE +SLAMEUSE +SLAVE +SLAVISANTES +SLAVISE +SLAVISERAS +SLAVISIONS +SLAVONNES +SLICAIT +SLICATES +SLICERAIT +SLICIEZ +SLOCHE +SLOVENE +SMALAH +SMARAGDINE +SMASHAI +SMASHASSIEZ +SMASHERAI +SMASHERONT +SMASHS +SMICS +SMILLAIT +SMILLATES +SMILLERAIT +SMILLIEZ +SMOG +SMORZANDO +SMURFAS +SMURFER +SMURFERIONS +SMURFIONS +SNIFAIENT +SNIFASSIONS +SNIFERAIENT +SNIFES +SNIFFAIS +SNIFFAT +SNIFFERAIS +SNIFFEUR +SNIFONS +SNOBANT +SNOBEE +SNOBERENT +SNOBINARDE +SNOOKER +SNOWPARK +SOBRIQUETS +SOCIABILISA +SOCIABILISAT +SOCIALISA +SOCIALISAS +SOCIALISE +SOCIALISERAS +SOCIALISIONS +SOCIATRIE +SOCIETAUX +SOCIOLECTES +SOCIOPATHIES +SOCLE +SOCS +SODES +SODOMISAI +SODOMISERAI +SODOMISERONT +SOEURETTES +SOFTBALLS +SOIF +SOIGNAIENT +SOIGNASSENT +SOIGNER +SOIGNERIONS +SOIGNEZ +SOIS +SOIXANTIEMES +SOLARIGRAPHE +SOLARISASSE +SOLARISEE +SOLARISERENT +SOLARISONS +SOLDANELLES +SOLDATES +SOLDERAI +SOLDERIONS +SOLDIONS +SOLEIL +SOLENNISES +SOLENOIDALE +SOLETTES +SOLFATARIENS +SOLFIAS +SOLFIEES +SOLFIEREZ +SOLI +SOLIDARISA +SOLIDARISER +SOLIDEMENT +SOLIDIFIASSE +SOLIDIFIEE +SOLIDIFIONS +SOLILOQUAMES +SOLILOQUE +SOLILOQUEREZ +SOLIN +SOLISTE +SOLIVEAU +SOLLICITAS +SOLLICITE +SOLLICITERAS +SOLLICITEUSE +SOLO +SOLSTICIALE +SOLUBILISAS +SOLUBILISEE +SOLUBILITE +SOLUTIONNAIS +SOLVABILISEE +SOLVABILITES +SOLVATES +SOMALIS +SOMATISAI +SOMATISER +SOMATOTROPE +SOMBRAIENT +SOMBRASSIONS +SOMBRERAIS +SOMBREROS +SOMITE +SOMMAIS +SOMMAT +SOMMEILLERAI +SOMMELIER +SOMMERAIENT +SOMMES +SOMMITALES +SOMNOLAMES +SOMNOLE +SOMNOLES +SONAGRAPHES +SONDAGE +SONDAMES +SONDE +SONDERAS +SONDEUSE +SONEGIENS +SONGEANT +SONGEES +SONGERENT +SONGEURS +SONGS +SONNAI +SONNAILLASSE +SONNAILLERA +SONNAMES +SONNASSIONS +SONNERAIENT +SONNERONS +SONNEUSES +SONORE +SONORISASSE +SONORISEE +SONORISERENT +SONORISONS +SOPALIN +SOPHISTIQUE +SOPHORA +SOPOR +SOPRANISTE +SORBET +SORBONNARD +SORDIDE +SORICIDES +SORORAUX +SORTAIS +SORTEURS +SORTILEGES +SORTIRENT +SORTISSE +SOSIES +SOTIES +SOTTISIERS +SOUBASSEMENT +SOUCHE +SOUCI +SOUCIASSENT +SOUCIER +SOUCIERIONS +SOUCIIONS +SOUDAGE +SOUDAINS +SOUDANIENNES +SOUDASSE +SOUDENT +SOUDERIEZ +SOUDIER +SOUDONS +SOUDOYASSENT +SOUDOYERENT +SOUFFERT +SOUFFLAIT +SOUFFLASSENT +SOUFFLEMENTS +SOUFFLEREZ +SOUFFLETAI +SOUFFLETEZ +SOUFFLEUR +SOUFFRAIENT +SOUFFRENT +SOUFFRIRA +SOUFFRIRONS +SOUFFRONS +SOUFRAI +SOUFRASSIEZ +SOUFRERAI +SOUFRERONT +SOUFRIONS +SOUHAITAIS +SOUHAITAT +SOUHAITERAIS +SOUHAITEZ +SOUILLAMES +SOUILLASSIEZ +SOUILLERAI +SOUILLERONT +SOUILLURES +SOUL +SOULAGEAS +SOULAGEMENT +SOULAGERAS +SOULAGIONS +SOULANTES +SOULASSIEZ +SOULE +SOULERAS +SOULEVA +SOULEVASSES +SOULEVENT +SOULEVERIEZ +SOULIER +SOULIGNAMES +SOULIGNE +SOULIGNERAIS +SOULIGNEZ +SOULONNE +SOUM +SOUMETTANT +SOUMIMES +SOUMISSIONNA +SOUNAS +SOUPANE +SOUPASSIONS +SOUPCONNAIT +SOUPCONNATES +SOUPENTES +SOUPERIEZ +SOUPESAIT +SOUPESATES +SOUPESERAIT +SOUPESIEZ +SOUPIEZ +SOUPIRANT +SOUPIRAT +SOUPIRES +SOUPLESSES +SOUQUASSE +SOUQUENILLE +SOUQUERENT +SOUQUONS +SOURCAS +SOURCEES +SOURCERAS +SOURCEUSE +SOURCILIERE +SOURCILLAS +SOURCILLER +SOURCILLONS +SOURDES +SOURIAIT +SOURICIERS +SOURIRAI +SOURIRIONS +SOURITES +SOUSCRIPTION +SOUSCRIRE +SOUSCRITS +SOUSCRIVIMES +SOUSCRIVITES +SOUSSIGNEES +SOUSTRACTIVE +SOUSTRAIRAS +SOUSTRAITES +SOUTACHA +SOUTACHASSES +SOUTACHERA +SOUTACHERONS +SOUTANES +SOUTENANCES +SOUTENEUSES +SOUTERRAIN +SOUTIENDRAIS +SOUTIENNES +SOUTINSSE +SOUTIRAI +SOUTIRASSIEZ +SOUTIRERAI +SOUTIRERONT +SOUTRAGES +SOUVENIONS +SOUVERAINE +SOUVIENDRA +SOUVIENDRONT +SOUVINSSENT +SOVIETIQUES +SOVIETISEES +SOVIETISEREZ +SOVIETOLOGIE +SOVNARKHOZES +SOYEZ +SPADICES +SPAHIS +SPAMMEUSES +SPARAGES +SPARIDES +SPARTEINES +SPASMODIQUE +SPASTICITE +SPATHIQUE +SPATIALISA +SPATIALISER +SPATIOLOGIE +SPATS +SPEAKEASY +SPECIALISA +SPECIALISER +SPECIALITES +SPECIFIAIENT +SPECIFIENT +SPECIFIERIEZ +SPECTRE +SPECULAI +SPECULASSENT +SPECULATION +SPECULER +SPECULERIONS +SPECULUM +SPEEDAIT +SPEEDATES +SPEEDERAIT +SPEEDIEZ +SPERGULAIRE +SPERMATICIDE +SPERME +SPERMOPHILE +SPHAIGNES +SPHENODONS +SPHERICITES +SPICIFORMES +SPILITE +SPINALIENNES +SPINNAKERS +SPINOZISTES +SPIRAL +SPIRE +SPIRITAIN +SPIROGRAPHE +SPIROMETRIE +SPIS +SPLANCHNIQUE +SPLENDEURS +SPLENITE +SPOLIAIENT +SPOLIASSIONS +SPOLIEE +SPOLIERENT +SPOLIONS +SPONDYLITE +SPONGIEUX +SPONSORING +SPONSORISEES +SPONTANE +SPONTANES +SPORANGE +SPOROGONE +SPORT +SPORTSMANS +SPORTWEARS +SPORULASSENT +SPORULER +SPORULERIONS +SPOULE +SPRAIERAIT +SPRAY +SPRAYASSENT +SPRAYER +SPRAYERIONS +SPRINTAIENT +SPRINTERAIT +SPRINTEUR +SPRUES +SPUMEUSES +SQUALIFORME +SQUAMEUX +SQUATINA +SQUATTAMES +SQUATTE +SQUATTERAS +SQUATTES +SQUAWS +SQUEEZASSENT +SQUEEZER +SQUEEZERIONS +SQUELETTIQUE +SQUIRREUX +STABILISANTS +STABILISERA +STABLES +STADIA +STAFFAI +STAFFASSIEZ +STAFFERAI +STAFFERONT +STAFFS +STAGNAIS +STAGNASSES +STAGNERA +STAGNERONS +STALINISMES +STAMINEE +STANDARD +STANDARDISEZ +STANDISTES +STANNIQUES +STAPHYLINS +STARIES +STARIFIASSE +STARIFIEE +STARIFIERENT +STARIFIONS +STARISASSENT +STARISEES +STARISEREZ +STARKING +STASES +STATIF +STATIONNAIS +STATIONNER +STATIQUEMENT +STATISTIQUES +STATTHALTER +STATUANT +STATUEE +STATUERENT +STATUFIA +STATUFIASSES +STATUFIERA +STATUFIERONS +STATURE +STAWUG +STEARINE +STEATITE +STEENBOK +STEGOMYA +STELE +STENCIL +STENOSAGES +STENOSASSES +STENOSERA +STENOSERONS +STENOTYPAI +STENOTYPERAI +STENT +STEPPE +STERADIAN +STERAS +STERCORAL +STEREES +STEREOMETRIE +STEREOTAXIE +STEREOTYPAT +STEREOTYPIE +STERERAIENT +STERES +STERILET +STERILISER +STERILITE +STERNALE +STERNUMS +STEROIDIENS +STERTOREUSES +STEWARDESS +STIBIEES +STIGMA +STIGMATISENT +STIGMOMETRE +STILTON +STIMULANT +STIMULAT +STIMULEES +STIMULEREZ +STIMULINES +STIPENDIAMES +STIPENDIE +STIPENDIERAS +STIPENDIIONS +STIPULAI +STIPULANTS +STIPULATIONS +STIPULERAIT +STIPULIEZ +STOCKAGES +STOCKASSES +STOCKERA +STOCKERONS +STOCKIONS +STOICIENS +STOLONIAL +STOMACAUX +STOMATOLOGIE +STOMISAIENT +STOMISES +STOP +STOPPAS +STOPPEES +STOPPEREZ +STOPPEZ +STOT +STRABIQUES +STRAMOINE +STRANGULANT +STRANGULIEZ +STRASS +STRATEGIQUE +STRATIFIAIT +STRATIFIATES +STRATIFIES +STRATIOMYS +STREAMING +STRESSA +STRESSAS +STRESSEES +STRESSEREZ +STRETCH +STRIAIT +STRIATES +STRIDENCE +STRIDULAIENT +STRIDULEES +STRIDULEREZ +STRIDULIEZ +STRIERAIENT +STRIES +STRIGILE +STRIOSCOPIES +STRIPPAIT +STRIPPATES +STRIPPERAIT +STRIPPEZ +STROBOSCOPE +STRONGYLE +STROPHANTE +STRUCTURAI +STRUCTUREL +STRUCTURIEZ +STUCAGE +STUDIEUSE +STUPA +STUPEFIAI +STUPEFIASSE +STUPEFIENT +STUPEFIERIEZ +STUPEURS +STUPRES +STUQUASSE +STUQUENT +STUQUERIEZ +STURNIDES +STYLASSENT +STYLER +STYLERIONS +STYLICIENS +STYLISAMES +STYLISATION +STYLISERAIS +STYLISEZ +STYLOGRAPHES +STYPTIQUES +SU +SUAMES +SUASSIONS +SUBAERIEN +SUBALPINES +SUBARIDE +SUBDELEGUAT +SUBDELEGUIEZ +SUBDIVISAMES +SUBDIVISE +SUBDIVISERAS +SUBDIVISION +SUBEREUSE +SUBIES +SUBIRAIS +SUBISSAIENT +SUBISSONS +SUBJACENTS +SUBJUGUAIENT +SUBJUGUES +SUBLAIT +SUBLATES +SUBLERAIT +SUBLIEZ +SUBLIMASSENT +SUBLIMEES +SUBLIMERENT +SUBLINGUALE +SUBMERGES +SUBODORA +SUBODORASSES +SUBODORERA +SUBODORERONS +SUBORBITAUX +SUBORDONNE +SUBORDONNONS +SUBORNASSENT +SUBORNEES +SUBORNEREZ +SUBORNEZ +SUBRECARGUE +SUBROGATIFS +SUBROGEAIENT +SUBROGES +SUBSEQUENTE +SUBSIDIAIRE +SUBSIDIASSE +SUBSIDIEE +SUBSIDIERENT +SUBSIDIONS +SUBSISTANTE +SUBSISTATES +SUBSISTERENT +SUBSISTONS +SUBSTANTIFS +SUBSTANTIVES +SUBSTITUERAI +SUBSTITUTIFS +SUBSTRUCTION +SUBSUMANT +SUBSUMEE +SUBSUMERENT +SUBSUMONS +SUBTILISAI +SUBTILISER +SUBTILS +SUBURBAINE +SUBVENIEZ +SUBVERSIVES +SUBVERTIRAIT +SUBVERTIT +SUBVINS +SUCAI +SUCASSIEZ +SUCCEDANE +SUCCEDERAIT +SUCCEDIEZ +SUCCESSEURES +SUCCESSIVE +SUCCOMBA +SUCCOMBASSES +SUCCOMBES +SUCCULENTE +SUCCUSSIONS +SUCERA +SUCERONS +SUCIEZ +SUCOTAIT +SUCOTATES +SUCOTERAIENT +SUCOTES +SUCRAIS +SUCRASSE +SUCREES +SUCREREZ +SUCREZ +SUCRIONS +SUDATOIRE +SUDORAL +SUDRA +SUEDOISE +SUERAIT +SUETTE +SUFFIRAI +SUFFIRONS +SUFFISANTE +SUFFISSE +SUFFIXAIS +SUFFIXASSES +SUFFIXEES +SUFFIXEREZ +SUFFOCANT +SUFFOQUAIENT +SUFFOQUES +SUFFRAGES +SUGGERAI +SUGGERASSIEZ +SUGGERERAI +SUGGERERONT +SUGGESTIF +SUGGESTIVITE +SUICIDANT +SUICIDAT +SUICIDERAIS +SUICIDEZ +SUIFA +SUIFASSES +SUIFERA +SUIFERONS +SUIFFANT +SUIFFEE +SUIFFERENT +SUIFFEZ +SUINT +SUINTANTS +SUINTEE +SUINTERAIT +SUINTIEZ +SUIT +SUIVANTES +SUIVI +SUIVISSE +SUIVRA +SUIVRONS +SULCATURE +SULFATAGES +SULFATASSES +SULFATENT +SULFATERIEZ +SULFATIEZ +SULFITANT +SULFITEE +SULFITERENT +SULFITONS +SULFONAIT +SULFONATES +SULFONES +SULFURAMES +SULFURATION +SULFURERAIS +SULFUREUSE +SULFURISEES +SULPICIENNES +SULVINITE +SUMMUMS +SUNNAS +SUPAIS +SUPAT +SUPERAI +SUPERE +SUPERFIN +SUPERFLUES +SUPERLATIFS +SUPERMALE +SUPERORDRES +SUPERPOSERA +SUPERPREFETS +SUPERS +SUPERSTRAT +SUPERVISAIT +SUPERVISATES +SUPERVISEURS +SUPERWOMAN +SUPINATIONS +SUPPLANTAMES +SUPPLANTEZ +SUPPLEANCE +SUPPLEASSIEZ +SUPPLEERAI +SUPPLEERONT +SUPPLIAI +SUPPLIASSE +SUPPLICES +SUPPLICIER +SUPPLIEES +SUPPLIEREZ +SUPPLIQUE +SUPPORTAIT +SUPPORTATES +SUPPORTERAIT +SUPPORTEUR +SUPPOSA +SUPPOSASSE +SUPPOSEMENT +SUPPOSEREZ +SUPPOSITIONS +SUPPRIMA +SUPPRIMASSE +SUPPRIMENT +SUPPRIMERIEZ +SUPPURAI +SUPPURASSE +SUPPURATIONS +SUPPURES +SUPPUTAMES +SUPPUTATION +SUPPUTERAIS +SUPPUTEZ +SUPREMUM +SURABONDANCE +SURABONDEZ +SURACTIVAIS +SURACTIVAT +SURACTIVEZ +SURAIGUES +SURAJOUTASSE +SURAJOUTENT +SURALE +SURALIMENTES +SURANNES +SURARMAS +SURARMEES +SURARMERAS +SURARMIONS +SURBAISSAMES +SURBAISSE +SURBAISSEZ +SURBOOKING +SURCHARGE +SURCHARGERA +SURCHAUFFAIT +SURCHAUFFE +SURCHAUFFIEZ +SURCLASSAIT +SURCLASSATES +SURCLASSES +SURCOMPOSEES +SURCOMPRIME +SURCONTRER +SURCOSTALES +SURCOTAS +SURCOTEES +SURCOTEREZ +SURCOTS +SURCOUPER +SURDETERMINA +SURDIMUTITES +SURDORAIS +SURDORAT +SURDORERAIS +SURDOREZ +SURDOSAIS +SURDOSAT +SURDOSERAIS +SURDOSEZ +SUREAUX +SURELEVAS +SURELEVE +SURELEVERAS +SURELEVIONS +SUREMINENTS +SURENCHERIRA +SURENDETTAI +SURENDETTERA +SURENTRAINEE +SUREPAISSEUR +SUREQUIPASSE +SUREQUIPONS +SURESTIMERA +SURETS +SUREVALUAS +SUREVALUE +SUREVALUERAS +SUREVALUIONS +SUREXCITAMES +SUREXCITERA +SUREXPLOITE +SUREXPOSA +SUREXPOSERA +SURFA +SURFACAS +SURFACEES +SURFACEREZ +SURFACIONS +SURFACTURAT +SURFACTURES +SURFAISAIS +SURFAIX +SURFATES +SURFERENT +SURFEUSES +SURFILAIT +SURFILATES +SURFILERAIT +SURFILIEZ +SURFINS +SURFITES +SURGE +SURGELASSENT +SURGELE +SURGELERAS +SURGELIONS +SURGEONNAI +SURGEONNEZ +SURGIR +SURGIRIONS +SURGONFLAI +SURGONFLERAI +SURHAUSSAI +SURHAUSSER +SURHUMAIN +SURIE +SURIMPOSAIS +SURIMPOSAT +SURIMPOSEZ +SURINAI +SURINATES +SURINERAIT +SURINEURS +SURINFECTANT +SURINFECTEES +SURINFORMA +SURINFORMER +SURINS +SURINVESTIE +SURIRAIENT +SURIS +SURISSIONS +SURJALANT +SURJALEE +SURJALERENT +SURJALONS +SURJETAIENT +SURJETEUSES +SURJEU +SURJOUASSE +SURJOUENT +SURJOUERIEZ +SURLIASSENT +SURLIER +SURLIERIONS +SURLIGNAIS +SURLIGNAT +SURLIGNERAIS +SURLIGNEUR +SURLIURES +SURLOUAS +SURLOUEES +SURLOUEREZ +SURLOYER +SURMENAMES +SURMENES +SURMONTABLES +SURMONTASSES +SURMONTERA +SURMONTERONS +SURMOULAS +SURMOULEES +SURMOULEREZ +SURMULET +SURNAGEAI +SURNAGERAIS +SURNAGEZ +SURNATURELS +SURNOMMANT +SURNOMMEE +SURNOMMERENT +SURNOMMONS +SURNOTASSE +SURNOTENT +SURNOTERIEZ +SUROXYDA +SUROXYDASSES +SUROXYDENT +SUROXYDERIEZ +SUROXYGENEE +SURPAIERAS +SURPASSAIS +SURPASSAT +SURPASSERAI +SURPASSERONT +SURPAYA +SURPAYASSES +SURPAYERA +SURPAYERONS +SURPEUPLEE +SURPIQUAIS +SURPIQUAT +SURPIQUERAIS +SURPIQUEZ +SURPLOMBA +SURPLOMBAS +SURPLOMBEES +SURPLOMBERAS +SURPLOMBIONS +SURPRENANT +SURPRENDRAS +SURPRENIONS +SURPRIS +SURPRODUITS +SURPROTEGEE +SURPUISSANCE +SURREACTION +SURREAGIRAS +SURREAGITES +SURREELLE +SURRENALE +SURSATURAI +SURSATURASSE +SURSATUREE +SURSATURONS +SURSAUTASSE +SURSAUTERA +SURSAUTERONS +SURSEMA +SURSEMASSES +SURSEMERA +SURSEMERONS +SURSISSIONS +SURSOLIDE +SURSOYIONS +SURSTOCKAMES +SURSTOCKE +SURSTOCKERAS +SURSTOCKIONS +SURTAXANT +SURTAXEE +SURTAXERENT +SURTAXONS +SURTITRAIT +SURTITRATES +SURTITRERAIT +SURTITRIEZ +SURTONDAIT +SURTONDIS +SURTONDRAI +SURTONDRONT +SURTRAVAIL +SURVECUMES +SURVEILLA +SURVEILLE +SURVEILLERAS +SURVEILLIONS +SURVENANTS +SURVENDIEZ +SURVENDIT +SURVENDREZ +SURVENEZ +SURVENTERA +SURVETEMENT +SURVIENDREZ +SURVIES +SURVINTES +SURVIRAS +SURVIRER +SURVIRERIONS +SURVIRIONS +SURVITRAGE +SURVIVANTS +SURVIVRAIENT +SURVOL +SURVOLASSENT +SURVOLER +SURVOLERIONS +SURVOLTAGE +SURVOLTER +SUS +SUSCITAMES +SUSCITE +SUSCITERAS +SUSCITIONS +SUSCRIRE +SUSCRITS +SUSCRIVIMES +SUSCRIVITES +SUSHI +SUSPECT +SUSPECTER +SUSPENDAIENT +SUSPENDIONS +SUSPENDONS +SUSPENDRIONS +SUSPENSES +SUSPENSOIDES +SUSPICIONS +SUSTENTAIT +SUSTENTATES +SUSTENTENT +SUSTENTERIEZ +SUSURRAI +SUSURRASSE +SUSURREE +SUSURRERAIT +SUSURRIEZ +SUTRAS +SUTURANT +SUTURE +SUTURERAS +SUTURIONS +SVASTIKA +SWAHILI +SWAPPAMES +SWAPPE +SWAPPERAS +SWAPPIONS +SWEATER +SWINGANTES +SWINGUAS +SWINGUER +SWINGUERIONS +SWITCHAI +SWITCHASSIEZ +SWITCHERAI +SWITCHERONT +SYBARITIQUES +SYENITE +SYLLABIQUE +SYLLOGISMES +SYLVAINS +SYLVINES +SYMBIOTES +SYMBOLISERA +SYMBOLISTES +SYMETRISAIT +SYMETRISATES +SYMETRISES +SYMPATHIE +SYMPATHISAT +SYMPATHISONS +SYMPHONISTES +SYMPOSIONS +SYNAGOGALES +SYNANTHERES +SYNARCHIES +SYNCHRONISEE +SYNCINESIES +SYNCOPAIS +SYNCOPASSES +SYNCOPER +SYNCOPERIONS +SYNCRETISME +SYNDACTYLIE +SYNDICALISME +SYNDICATS +SYNDIQUAS +SYNDIQUEES +SYNDIQUEREZ +SYNDROME +SYNERESE +SYNESTHESIE +SYNODE +SYNONYMIQUE +SYNOSTOSES +SYNTHESE +SYNTHETISAI +SYNTHETISER +SYNTHETISONS +SYNTONISAMES +SYNTONISERAI +SYRIENNE +SYRPHIDE +SYSTEMATISEZ +SYZYGIE +TABACOLOGUE +TABAGIES +TABARD +TABASSAI +TABASSASSIEZ +TABASSERAI +TABASSERONT +TABELLE +TABLAI +TABLAS +TABLE +TABLERAI +TABLERONT +TABLETTAIENT +TABLETTERONS +TABLIER +TABLONS +TABOUAIT +TABOUATES +TABOUERAIT +TABOUIEZ +TABOUISASSE +TABOUISEE +TABOUISERENT +TABOUISONS +TABULAIRE +TABULASSIEZ +TABULE +TABULERAS +TABULIONS +TACCAS +TACHAS +TACHEES +TACHER +TACHERIONS +TACHETAIS +TACHETAT +TACHETIONS +TACHETTEREZ +TACHINA +TACHISMES +TACHONS +TACHYGRAPHES +TACLAIENT +TACLASSIONS +TACLERAIENT +TACLES +TACOT +TACTILE +TADJIKES +TAENIASIS +TAGALOG +TAGGAIS +TAGGAT +TAGGERAIS +TAGGES +TAGMES +TAGUASSE +TAGUENT +TAGUERIEZ +TAGUIEZ +TAIGA +TAILLADAI +TAILLADERAI +TAILLADERONT +TAILLAIS +TAILLASSE +TAILLEE +TAILLERENT +TAILLEURS +TAILLOLES +TAIRAIT +TAISAIS +TAISIEZ +TAKAHE +TALAMES +TALAT +TALEDS +TALENTUEUX +TALERIEZ +TALEVES +TALIONS +TALITRES +TALLAS +TALLER +TALLERIONS +TALLIPOTS +TALMUDISTES +TALOCHANT +TALOCHEE +TALOCHERENT +TALOCHONS +TALONNAIT +TALONNATES +TALONNES +TALONNIEZ +TALQUAIENT +TALQUASSIONS +TALQUERAIENT +TALQUES +TALUS +TALUTAS +TALUTEES +TALUTEREZ +TALWEG +TAMARINADE +TAMAZIRT +TAMBOURINER +TAMBOURINONS +TAMILE +TAMISAIT +TAMISATES +TAMISERAIT +TAMISES +TAMISIEZ +TAMPICOS +TAMPONNAIT +TAMPONNATES +TAMPONNES +TAMPONNOIR +TANCAS +TANCEES +TANCEREZ +TANCIONS +TANDOORS +TANGENCES +TANGENTASSE +TANGENTENT +TANGENTERIEZ +TANGENTIEZ +TANGIBILITES +TANGUA +TANGUASSES +TANGUERAIENT +TANGUES +TANINS +TANISAMES +TANISE +TANISERAS +TANISIONS +TANNA +TANNANTES +TANNATS +TANNERAIT +TANNES +TANNIQUE +TANNISANT +TANNISEE +TANNISERENT +TANNISONS +TANTE +TANTRIQUE +TAOISMES +TAPAGEAIENT +TAPAGERAIT +TAPAGEURS +TAPAIT +TAPASSIEZ +TAPEMENTS +TAPERAS +TAPEUR +TAPIEZ +TAPINAS +TAPINER +TAPINERIONS +TAPINONS +TAPIRAIT +TAPISSA +TAPISSANTES +TAPISSE +TAPISSERAS +TAPISSEUR +TAPISSIONS +TAPOCHANT +TAPOCHEE +TAPOCHERENT +TAPOCHONS +TAPONNANT +TAPONNEE +TAPONNERENT +TAPONNEUSES +TAPOTAIENT +TAPOTASSIONS +TAPOTERA +TAPOTERONS +TAPURES +TAQUAMES +TAQUE +TAQUERAS +TAQUEZ +TAQUINAS +TAQUINEES +TAQUINEREZ +TAQUINIONS +TARABISCOTA +TARABISCOTEZ +TARABUSTAMES +TARABUSTE +TARABUSTERAS +TARABUSTIONS +TARAMA +TARASQUE +TARAUD +TARAUDANTE +TARAUDATES +TARAUDERAIT +TARAUDEURS +TARBAIS +TARDAIENT +TARDASSIONS +TARDERA +TARDERONS +TARDILLONNE +TARE +TARENTES +TARERAIENT +TARES +TARGUAIENT +TARGUASSIONS +TARGUERAIENT +TARGUES +TARGUONS +TARIFA +TARIFASSE +TARIFENT +TARIFERIEZ +TARIFIAIT +TARIFIATES +TARIFIES +TARIN +TARIRAIENT +TARIS +TARISSENT +TARMAC +TAROLOGUE +TAROTEURS +TARSECTOMIE +TARSIIFORMES +TARTARINS +TARTIGNOLE +TARTINAIS +TARTINAT +TARTINERAIS +TARTINEZ +TARTIRAS +TARTISSAIT +TARTITES +TARTREUSES +TARTUFFERIES +TASMANIENS +TASSAS +TASSEAUX +TASSERAIS +TASSETTE +TAT +TATANES +TATAOUINANT +TATAOUINENT +TATARE +TATATES +TATERAIT +TATEURS +TATILLONNAGE +TATILLONNEZ +TATONNAI +TATONNASSE +TATONNENT +TATONNERIEZ +TATOU +TATOUAS +TATOUEES +TATOUEREZ +TATOUEZ +TAUDA +TAUDASSES +TAUDERA +TAUDERONS +TAULARDE +TAUONS +TAUPASSENT +TAUPER +TAUPERIONS +TAUPIN +TAUREAUX +TAUROBOLES +TAUTOLOGIES +TAUZIN +TAVELAI +TAVELASSIEZ +TAVELEZ +TAVELLERAS +TAVELURES +TAXABLE +TAXAMES +TAXATEUR +TAXAUDIER +TAXERAIS +TAXEZ +TAXIE +TAXINOMIE +TAXISTE +TAXOL +TAXUM +TAYLORISERA +TBILISSIENNE +TCHADIQUES +TCHATCHAMES +TCHATCHE +TCHATCHERAS +TCHATCHEUSE +TCHATTAIS +TCHATTAT +TCHATTERAS +TCHATTEUSE +TCHEQUE +TCHERVONETS +TCHOUKTCHES +TEC +TECHNICIENS +TECHNICISENT +TECHNICISTE +TECHNISAI +TECHNISERAI +TECHNISERONT +TECHNOPHILE +TECK +TECTONIQUE +TEES +TEFLONISEES +TEGUMENTS +TEIGNES +TEIGNISSE +TEILLAGES +TEILLASSES +TEILLERA +TEILLERONS +TEILLONS +TEINDRIONS +TEINTAMES +TEINTASSIONS +TEINTERAIENT +TEINTES +TEINTURIER +TEKES +TELECABINES +TELECHARGEE +TELECOM +TELECOPIER +TELECRAN +TELEDIFFUSER +TELEGA +TELEGRAPHE +TELEGRAPHIEZ +TELEGUIDA +TELEGUIDASSE +TELEGUIDENT +TELEMATISANT +TELEMATISE +TELEMATISONS +TELEMETRAMES +TELEMETRE +TELEMETRERAS +TELEMETREUSE +TELENCEPHALE +TELEOLOGIQUE +TELEOSAURE +TELEPATHIQUE +TELEPHONA +TELEPHONASSE +TELEPHONENT +TELEPHONIQUE +TELEPORTERA +TELERADAR +TELEROMAN +TELESCOPAIS +TELESCOPAT +TELESCOPEZ +TELESERVICES +TELESTHESIE +TELETEXTES +TELETRANSMIS +TELEVERITES +TELEVISER +TELEVISONS +TELEXAIT +TELEXATES +TELEXERAIT +TELEXIEZ +TELLIERES +TELLUREUX +TELLURISMES +TELOLECITHES +TELSON +TEMOIGNAS +TEMOIGNEES +TEMOIGNEREZ +TEMOIN +TEMPERANTS +TEMPERATURES +TEMPERERAIT +TEMPERIEZ +TEMPETANT +TEMPETENT +TEMPETERIEZ +TEMPETUEUSES +TEMPORAIRE +TEMPORELLE +TEMPORISANT +TEMPORISERA +TEMPS +TENAIENT +TENAILLANTS +TENAILLEE +TENAILLERAIT +TENAILLIEZ +TENANTE +TENDANCIELLE +TENDE +TENDEURS +TENDINITES +TENDITES +TENDRAS +TENDRIONS +TENEBREUSE +TENELIENNES +TENEUSE +TENIEZ +TENNISTIQUES +TENONNA +TENONNASSE +TENONNENT +TENONNERIEZ +TENONNONS +TENORISAIT +TENORISERAI +TENORISERONT +TENOTOMIES +TENSONS +TENTAI +TENTASSE +TENTATIONS +TENTERA +TENTERONS +TENTURES +TENUS +TEPALES +TEPHROSIES +TERAGONES +TERATOLOGUES +TERCAI +TERCASSIEZ +TERCERAI +TERCERONT +TEREBELLUM +TEREBIQUE +TEREPHTALATE +TERGALE +TERGIVERSAIT +TERGIVERSONS +TERMINAI +TERMINANT +TERMINATEURS +TERMINERAIS +TERMINEZ +TERNAIRES +TERNIRAIENT +TERNIS +TERNISSEZ +TERPENIQUE +TERRA +TERRAIT +TERRAQUES +TERRASSANT +TERRASSEE +TERRASSERAIT +TERRASSIER +TERREAU +TERREAUTAS +TERREAUTEES +TERREAUTEREZ +TERREAUX +TERRER +TERRERIONS +TERREUX +TERRIENNES +TERRIFIAIS +TERRIFIASSES +TERRIFIERA +TERRIFIERONS +TERRILS +TERRIRAIENT +TERRIS +TERRISSIONS +TERRORISAI +TERRORISASSE +TERRORISENT +TERRORISONS +TERSAS +TERSEES +TERSEREZ +TERTIAIRE +TERVUEREN +TESSELLES +TEST +TESTACELLES +TESTAT +TESTER +TESTERIONS +TESTIONS +TETAI +TETANISAI +TETANISER +TETANOS +TETAT +TETERAI +TETERIONS +TETINES +TETRACORDE +TETRADYNAME +TETRALINE +TETRAPHONIE +TETRAPODE +TETRAS +TETRAVALENCE +TETUS +TEUTONIQUE +TEXAN +TEXTANT +TEXTEE +TEXTERENT +TEXTILES +TEXTURA +TEXTURASSENT +TEXTUREES +TEXTUREREZ +TEXTURISAI +TEXTURISER +TEZIGUE +THALAMUS +THALLIUMS +THANES +THEATRAL +THEATRALITES +THEBAIN +THEIERES +THEMATIQUE +THEMATISAS +THEMATISE +THEMATISERAS +THEMATISIONS +THEOGONIE +THEOLOGIENNE +THEOPHORES +THEORETIQUES +THEORISA +THEORISASSES +THEORISENT +THEORISERIEZ +THEOSOPHES +THERIDIUMS +THERMALITE +THERMIDOR +THERMIQUES +THERMISEES +THERMISEREZ +THERMISTANCE +THERMOCHIMIE +THERMOHALIN +THERMOPHILE +THERMOSIPHON +THEROPODE +THESAURISAI +THESAURISERA +THESAURUS +THETA +THEURGIQUES +THIONATE +THIOPENTALS +THIXOTROPIES +THOMISIDES +THONIERES +THORAX +THORONS +THRIDACES +THROMBOLYSE +THULIUMS +THURINGIENNE +THYIADES +THYMIQUE +THYRATRON +THYRISTORS +THYROIDIENS +TIAFFES +TIBIAL +TICHODROME +TICTAQUAI +TICTAQUERAIS +TICTAQUEZ +TIDJANISME +TIEDEURS +TIEDIRAIT +TIEDISSAIS +TIEDISSIONS +TIENDRAIS +TIENNES +TIERCAIS +TIERCAT +TIERCELETS +TIERCERAS +TIERCIEZ +TIFOSIS +TIGEAMES +TIGELLE +TIGERAS +TIGEZ +TIGRAIS +TIGRAT +TIGRERAIS +TIGRESSE +TIGRONS +TILDE +TILLAGES +TILLANT +TILLEE +TILLERENT +TILLEURS +TILTA +TILTASSES +TILTERAIENT +TILTES +TIMBRA +TIMBRASSE +TIMBRENT +TIMBRERIEZ +TIMBRIEZ +TIMON +TIMORAISES +TINCTORIAL +TINMES +TINTAI +TINTASSENT +TINTEMENTS +TINTEREZ +TINTOUINS +TIPASSE +TIPENT +TIPERIEZ +TIPONS +TIPPASSENT +TIPPER +TIPPERIONS +TIPULES +TIQUASSENT +TIQUERAI +TIQUERONT +TIQUEUSE +TIRAGE +TIRAILLANT +TIRAILLEE +TIRAILLERAIT +TIRAILLES +TIRAMISU +TIRASSES +TIRELIRES +TIREREZ +TIRETTE +TIROIRS +TISANERIE +TISASSIEZ +TISERAIS +TISEZ +TISONNANT +TISONNEE +TISONNERENT +TISONNIEZ +TISSAIT +TISSATES +TISSERAIT +TISSERINS +TISSIEZ +TISSUS +TITANS +TITILLAIT +TITILLATES +TITILLES +TITRA +TITRANT +TITREE +TITRERENT +TITREUSES +TITRISAIENT +TITRISERA +TITRISERONS +TITUBAIENT +TITUBASSENT +TITUBER +TITUBERIONS +TITULARISA +TITULARISEES +TITULATURES +TO +TOASTAIT +TOASTATES +TOASTERAIT +TOASTEUR +TOBAGONIENS +TOCARD +TOCSIN +TOFFEES +TOILAGE +TOILASSENT +TOILER +TOILERIES +TOILETTAIENT +TOILETTES +TOILEUSES +TOISAI +TOISASSIEZ +TOISERAI +TOISERONT +TOITURES +TOKHARIENNE +TOLAI +TOLARDS +TOLATES +TOLER +TOLERANCES +TOLERASSES +TOLERERA +TOLERERONS +TOLES +TOLIEZ +TOLSTOIENS +TOLUS +TOMAIT +TOMASSIONS +TOMBAIENT +TOMBANTES +TOMBAUX +TOMBERA +TOMBERIEZ +TOMBEZ +TOMEES +TOMERAIT +TOMETTES +TOMMETTE +TONAUX +TONDAIT +TONDIEZ +TONDIT +TONDRAS +TONDUES +TONGANS +TONIC +TONIFIAIENT +TONIFIASSENT +TONIFIER +TONIFIERIONS +TONITRUA +TONITRUAS +TONITRUER +TONKINOIS +TONNAIS +TONNASSES +TONNELET +TONNERA +TONNERONS +TONOGRAPHIES +TONSURA +TONSURASSES +TONSURERA +TONSURERONS +TONTINAI +TONTINASSIEZ +TONTINERAI +TONTINERONT +TONTISSE +TOPAIENT +TOPASSIONS +TOPERA +TOPERONS +TOPHUS +TOPO +TOPOLOGIES +TOPONYMIE +TOQUA +TOQUANTES +TOQUASSIONS +TOQUERAIENT +TOQUES +TORAILLAI +TORAILLERAIS +TORAILLEZ +TORCHA +TORCHASSES +TORCHERA +TORCHERIEZ +TORCHONNA +TORCHONNERA +TORCOU +TORDANTES +TORDIEZ +TORDIT +TORDRAS +TORDUES +TOREAMES +TOREE +TOREERAS +TOREIONS +TORGNOLES +TORNADE +TORONNAIS +TORONNAT +TORONNERAIS +TORONNEUSE +TORPEDO +TORPILLAIENT +TORPILLERONS +TORQUES +TORRAILLASSE +TORRAILLERA +TORREFIAS +TORREFIEES +TORREFIEREZ +TORRENT +TORRENTUEUX +TORSADAMES +TORSADE +TORSADERAS +TORSADIONS +TORTELLINI +TORTILLAIS +TORTILLER +TORTILLEREZ +TORTILLONS +TORTORAI +TORTORASSIEZ +TORTORERAI +TORTORERONT +TORTUE +TORTURAIENT +TORTURASSENT +TORTURER +TORTURERIONS +TORVES +TOSCANES +TOSSAMES +TOSSE +TOSSEREZ +TOT +TOTALISAMES +TOTALISEE +TOTALISERENT +TOTALISIEZ +TOTEM +TOTEMISAS +TOTEMISE +TOTEMISERAS +TOTEMISIONS +TOTIPOTENTS +TOUAI +TOUAREGUE +TOUBAB +TOUCHAIENT +TOUCHASSENT +TOUCHE +TOUCHERAIS +TOUCHES +TOUCHONS +TOUERAIENT +TOUES +TOUFFU +TOUILLAIS +TOUILLAT +TOUILLERAIS +TOUILLETTE +TOUIONS +TOULOUSAINE +TOUPAYE +TOUPILLAIENT +TOUPILLES +TOUPIN +TOUPINASSENT +TOUPINERAI +TOUPINERONT +TOURA +TOURAILLONS +TOURANIEN +TOURASSIONS +TOURBAS +TOURBEES +TOURBEREZ +TOURBIER +TOURE +TOURERAIS +TOURET +TOURILLON +TOURISMES +TOURMALINES +TOURMENTE +TOURMENTERAS +TOURMENTEUSE +TOURNAGE +TOURNAILLANT +TOURNAILLER +TOURNASSENT +TOURNEBOULEE +TOURNEBRIDES +TOURNER +TOURNERIES +TOURNEUR +TOURNICOTAIT +TOURNICOTE +TOURNIOLE +TOURNIQUAS +TOURNIQUER +TOURNIS +TOURNOIS +TOURNOYANTES +TOURNOYE +TOURON +TOURTEREAUX +TOUSSAI +TOUSSAILLES +TOUSSANT +TOUSSENT +TOUSSERIE +TOUSSEUSES +TOUSSOTAMES +TOUSSOTE +TOUSSOTERAS +TOUSSOTIONS +TOUTS +TOXICO +TOXICOMANE +TOXICOSES +TOXOPLASMES +TRABENDISTE +TRABOULAMES +TRABOULE +TRABOULEREZ +TRAC +TRACAIS +TRACASSAIENT +TRACASSER +TRACASSERIES +TRACASSIERS +TRACEES +TRACERAI +TRACERIONS +TRACHEALE +TRACHEIDES +TRACHYTIQUE +TRACONS +TRACTAIS +TRACTASSIEZ +TRACTER +TRACTERIONS +TRACTION +TRACTORISTES +TRADEURS +TRADITIONS +TRADUIRAIS +TRADUISAIENT +TRADUISIEZ +TRADUISIT +TRAFICOTA +TRAFICOTASSE +TRAFICOTENT +TRAFICOTIEZ +TRAFIQUANT +TRAFIQUAT +TRAFIQUERAIS +TRAFIQUEUR +TRAGEDIENNE +TRAGIQUES +TRAHIRAI +TRAHIRONT +TRAHISSES +TRAIL +TRAINAILLA +TRAINAILLES +TRAINANTES +TRAINASSAIS +TRAINASSAT +TRAINASSEZ +TRAINEES +TRAINERAS +TRAINEUR +TRAINIONS +TRAIRAS +TRAITABLE +TRAITANTS +TRAITEE +TRAITERAIT +TRAITEURS +TRAITRISE +TRALEE +TRALUIRE +TRALUISAIT +TRALUISIS +TRALUITES +TRAMAIT +TRAMATES +TRAMERAIENT +TRAMES +TRAMINOTS +TRANCHAIENT +TRANCHASSENT +TRANCHEFILA +TRANCHEFILAT +TRANCHERAIS +TRANCHET +TRANCHOIRS +TRANSAMINASE +TRANSBAHUTAI +TRANSBORDENT +TRANSCENDAI +TRANSCODANT +TRANSCODEE +TRANSCODIEZ +TRANSCRIREZ +TRANSFECTION +TRANSFERAMES +TRANSFERAT +TRANSFERER +TRANSFERT +TRANSFIGURER +TRANSFILAIT +TRANSFILATES +TRANSFILIEZ +TRANSFORMA +TRANSFORMEZ +TRANSFUGE +TRANSFUSASSE +TRANSFUSENT +TRANSGENOSE +TRANSGRESSER +TRANSHUMANCE +TRANSHUMERAI +TRANSIGEA +TRANSIGES +TRANSIRAIS +TRANSISSONS +TRANSIT +TRANSITAS +TRANSITEES +TRANSITEREZ +TRANSITION +TRANSITOIRE +TRANSLATAI +TRANSLATERAI +TRANSLATIVE +TRANSLITERAS +TRANSLITERER +TRANSMANCHE +TRANSMETTEUR +TRANSMIGRER +TRANSMISE +TRANSMIT +TRANSMUAMES +TRANSMUE +TRANSMUERAS +TRANSMUIONS +TRANSMUTAMES +TRANSMUTERA +TRANSPADANES +TRANSPARENTS +TRANSPARUT +TRANSPIRA +TRANSPIRAS +TRANSPIRE +TRANSPIRERAS +TRANSPIRIONS +TRANSPORTENT +TRANSPOSAMES +TRANSPOSE +TRANSPOSERAS +TRANSPOSIONS +TRANSSUDAS +TRANSSUDATS +TRANSSUDIEZ +TRANSVASEREZ +TRANSVERSAL +TRANSVIDA +TRANSVIDERA +TRANSYLVAINS +TRAPEZOEDRE +TRAPP +TRAPPASSENT +TRAPPER +TRAPPERIONS +TRAPPILLON +TRAPUE +TRAQUAS +TRAQUEES +TRAQUERAIS +TRAQUET +TRASH +TRAUMATISERA +TRAVAILLAMES +TRAVAILLES +TRAVAILLISTE +TRAVAILLOTAS +TRAVELINGS +TRAVERSAIENT +TRAVERSER +TRAVERSIN +TRAVESTIES +TRAVESTIREZ +TRAVESTIT +TRAYEUSE +TREBUCHAIS +TREBUCHASSES +TREBUCHENT +TREBUCHERIEZ +TREBUCHONS +TREFILAMES +TREFILE +TREFILERAS +TREFILEUR +TREFLEES +TREFONDS +TREILLAGEA +TREILLAGERAI +TREILLES +TREILLISSER +TREIZIEMES +TREKS +TREMATAI +TREMATASSIEZ +TREMATERAI +TREMATERONT +TREMBLAIE +TREMBLAS +TREMBLEES +TREMBLERAS +TREMBLEUSE +TREMBLOTAIT +TREMBLOTERAI +TREMIERES +TREMOUSSAIT +TREMOUSSATES +TREMOUSSES +TREMPAI +TREMPASSIEZ +TREMPERAI +TREMPERONT +TREMPLINS +TREMULANTES +TREMULATION +TREMULERAIS +TREMULEZ +TRENTAIN +TRENTINE +TREPANANT +TREPANATIONS +TREPANERAIT +TREPANG +TREPASSAIS +TREPASSAT +TREPASSERAIS +TREPASSEZ +TREPIDAIENT +TREPIDASSENT +TREPIDER +TREPIDERIONS +TREPIGNA +TREPIGNASSES +TREPIGNENT +TREPIGNERIEZ +TREPIGNONS +TRESCHEUR +TRESSA +TRESSAILLIRA +TRESSAIT +TRESSATES +TRESSAUTER +TRESSEES +TRESSEREZ +TRESSEZ +TREUILLAI +TREUILLERAI +TREUILLERONT +TREVIRAI +TREVIRASSIEZ +TREVIRERAI +TREVIRERONT +TREVISE +TRIAGE +TRIALCOOLS +TRIANDINES +TRIANGULAIS +TRIANGULAT +TRIANGULERAI +TRIASSE +TRIATHLONIEN +TRIBADES +TRIBALLA +TRIBALLASSES +TRIBALLERA +TRIBALLERONS +TRIBASIQUE +TRIBOMETRIE +TRIBULATION +TRIBUTS +TRICARDS +TRICERATOPS +TRICHASSENT +TRICHER +TRICHERIES +TRICHEZ +TRICHINES +TRICHIURIDES +TRICHOLOGIE +TRICHOPHYTIE +TRICK +TRICOLAI +TRICOLASSIEZ +TRICOLERAIS +TRICOLEZ +TRICOTAIS +TRICOTAT +TRICOTERAIS +TRICOTETS +TRICOTONS +TRICYCLE +TRIDENTEE +TRIDIS +TRIENNATS +TRIERARQUES +TRIES +TRIEUSES +TRIFOLIEES +TRIFORIUM +TRIFOUILLER +TRIGEMINEE +TRIGLYPHES +TRILATERALES +TRILLAIENT +TRILLASSIONS +TRILLERAIENT +TRILLES +TRILOBITE +TRIMA +TRIMARDA +TRIMARDASSES +TRIMARDERA +TRIMARDERONS +TRIMAS +TRIMBALAGES +TRIMBALASSES +TRIMBALENT +TRIMBALERIEZ +TRIMBALLAGES +TRIMBALLENT +TRIME +TRIMERENT +TRIMESTRIEL +TRIMEUSE +TRIMURTI +TRINGLAI +TRINGLASSIEZ +TRINGLERAI +TRINGLERONT +TRINQUANT +TRINQUATES +TRINQUERAIT +TRINQUETS +TRINQUONS +TRIOLS +TRIOMPHASSE +TRIOMPHERAS +TRIOMPHIONS +TRIPAIENT +TRIPANT +TRIPARTITE +TRIPAT +TRIPERENT +TRIPETTES +TRIPIER +TRIPLAIENT +TRIPLASSES +TRIPLENT +TRIPLERIEZ +TRIPLEZ +TRIPLONS +TRIPOLIS +TRIPOTAGE +TRIPOTASSENT +TRIPOTER +TRIPOTERIONS +TRIPOTIONS +TRIQUAIENT +TRIQUASSE +TRIQUEE +TRIQUERENT +TRIQUETS +TRIREGNES +TRISKELE +TRISOCS +TRISSAMES +TRISSE +TRISSERAS +TRISSIONS +TRISTESSES +TRITURABLE +TRITURASSENT +TRITURE +TRITURERAS +TRITURIONS +TRIVALENCE +TRIVIALEMENT +TROCHAIQUE +TROCHES +TROCHISQUES +TROCHOPHORES +TROGLODYTE +TROGONIDES +TROLL +TROMBIDIONS +TROMBONES +TROMPAMES +TROMPE +TROMPERAS +TROMPETA +TROMPETASSES +TROMPETES +TROMPETTERAI +TROMPETTES +TROMPILLON +TRONANT +TRONCATION +TRONCHAIT +TRONCHATES +TRONCHERAIT +TRONCHETS +TRONCONNAGES +TRONCONNENT +TRONCONNIEZ +TRONERA +TRONERONS +TRONQUAIS +TRONQUAT +TRONQUERAIS +TRONQUEZ +TROPEZIENNE +TROPICALISE +TROPOPAUSES +TROQUAMES +TROQUE +TROQUERAS +TROQUEUR +TROTSKISME +TROTTAIENT +TROTTASSIONS +TROTTERAIENT +TROTTES +TROTTINAIENT +TROTTINES +TROTTIONS +TROUAMES +TROUBADE +TROUBLANT +TROUBLAT +TROUBLERAI +TROUBLERONT +TROUER +TROUERIONS +TROUFIONS +TROUILLARDE +TROUILLATES +TROUILLERENT +TROUPIER +TROUSSAIS +TROUSSAT +TROUSSER +TROUSSERIONS +TROUVA +TROUVANT +TROUVEE +TROUVERE +TROUVEURS +TROYENNES +TRUANDAMES +TRUANDE +TRUANDERAS +TRUANDEZ +TRUCAGE +TRUCIDANT +TRUCIDEE +TRUCIDERENT +TRUCIDONS +TRUCULENTES +TRUFFADES +TRUFFASSES +TRUFFERA +TRUFFERONS +TRUIES +TRUQUAIENT +TRUQUASSIONS +TRUQUERAIENT +TRUQUES +TRUQUONS +TRUSQUINASSE +TRUSQUINENT +TRUST +TRUSTASSENT +TRUSTER +TRUSTERIONS +TRUSTONS +TRYPSINOGENE +TSAREVITCHS +TSETSE +TSUGA +TU +TUAMES +TUASSIONS +TUBAIRES +TUBASSE +TUBELESS +TUBERAIS +TUBERCULIDE +TUBERCULINES +TUBERCULOSES +TUBERISATION +TUBES +TUBIPORES +TUBULEE +TUBULINE +TUDESQUE +TUERAIENT +TUERONS +TUFEAUX +TUFTEES +TUILAMES +TUILE +TUILERAIS +TUILERONT +TUILIONS +TULLERIE +TULLOISE +TUMEFIAIENT +TUMEFIES +TUMESCENTES +TUMULAIRE +TUNAGE +TUNGSTENE +TUNIQUE +TUNISOISES +TUPI +TURBEH +TURBINAIT +TURBINATES +TURBINES +TURBOMOTEUR +TURBULENCES +TURCO +TURDIDES +TURGESCENT +TURIONS +TURLUPINES +TURLUTAIENT +TURLUTASSES +TURLUTERA +TURLUTERONS +TURNE +TURPIDE +TURQUETTES +TURRITELLES +TUSSENT +TUT +TUTEURAI +TUTEURASSIEZ +TUTEURERAI +TUTEURERONT +TUTIES +TUTOIERAS +TUTORIELLE +TUTOYAS +TUTOYEES +TUTOYIONS +TUTU +TUYAUTAI +TUYAUTASSIEZ +TUYAUTERAI +TUYAUTERIONS +TUYAUTIONS +TWEETERS +TWISTAIENT +TWISTASSIONS +TWISTERAIENT +TWISTES +TYCOON +TYMPANISA +TYMPANISERA +TYMPANONS +TYPAIENT +TYPASSIONS +TYPERAIENT +TYPES +TYPHLITE +TYPHON +TYPIQUEMENT +TYPOGRAPHIEE +TYPOTE +TYRANNICIDES +TYRANNISES +TYRIENNES +TYROTHRICINE +TZARISTES +UBIQUISTE +UCHRONIQUE +UGNI +UKULELE +ULCERASSE +ULCERATIONS +ULCERERAIENT +ULCERES +ULEMA +ULLUCUS +ULNAIRE +ULTIMATUM +ULTRACOURT +ULTRAFILTRES +ULTRALEGERS +ULTRAMODERNE +ULTRAPLATE +ULTRARAPIDES +ULTRASECRETS +ULTRAVIDES +ULULAIENT +ULULASSIONS +ULULERA +ULULERONS +UMBANDAS +UNANIMES +UNCIFORME +UNETELLE +UNIATES +UNICITES +UNIEME +UNIFIAI +UNIFIASSIEZ +UNIFIE +UNIFIERAS +UNIFIIONS +UNIFORME +UNIFORMISAS +UNIFORMISEE +UNIFORMITE +UNILOBE +UNIMODALES +UNIONISTES +UNIRAI +UNIREZ +UNISEXUE +UNISSAIT +UNIT +UNITARISTES +UNIVALENT +UNIVOLTINS +UPAS +UPERISASSENT +UPERISEES +UPERISEREZ +UPPERCUT +URANAIS +URANIDE +URANISME +URANOPLASTIE +URBAINES +URBANISANT +URBANISERAIT +URBANISIEZ +URBANITES +UREASE +UREIDE +UREOTELIQUE +URETERITES +URETRAUX +URGEASSENT +URGENTISTE +URGERONT +URIDINES +URINANT +URINE +URINERAS +URINEUX +URIQUES +UROCHROMES +UROGRAPHIES +UROLOGUES +UROPYGIALE +URSIDES +URTICAIRES +URUBUS +USAGE +USAIS +USASSE +USENT +USERIEZ +USINABLE +USINAS +USINEES +USINEREZ +USINIERS +USNEES +USUEL +USURIERES +USURPASSE +USURPATIONS +USURPERA +USURPERONS +UTERIN +UTILISABLE +UTILISASSENT +UTILISATRICE +UTILISERAIS +UTILISEZ +UTILITE +UTRAQUISTE +UVALE +UVULAS +UZBEKS +VACANTES +VACCIN +VACCINALES +VACCINATES +VACCINEES +VACCINERAS +VACCINIDES +VACCINOIDES +VACHAIT +VACHASSES +VACHER +VACHEREZ +VACHES +VACILLAI +VACILLASSE +VACILLEMENT +VACILLERENT +VACILLONS +VACUOLES +VACUOLISEES +VACUOLISEREZ +VACUOME +VADROUILLAIT +VADROUILLE +VADROUILLIEZ +VAGABONDAIT +VAGABONDATES +VAGABONDONS +VAGIR +VAGIRIONS +VAGISSANTS +VAGIT +VAGUA +VAGUASSES +VAGUEMESTRES +VAGUEREZ +VAHINE +VAILLANCES +VAIN +VAINCRIEZ +VAINEMENT +VAINQUEURE +VAINQUISSENT +VAIREE +VAISSEAUX +VAJRAYANAS +VALAISANS +VALDEISMES +VALDINGUERAI +VALENCIA +VALENTINITE +VALGUS +VALIDASSENT +VALIDEES +VALIDERENT +VALIDIEZ +VALIONS +VALLEES +VALLONNAIT +VALLONNATES +VALLONNES +VALOCHES +VALORISAMES +VALORISERA +VALORISERONS +VALSA +VALSASSES +VALSERA +VALSERONS +VALSONS +VALUSSIEZ +VALVULAIRE +VAMP +VAMPASSENT +VAMPER +VAMPERIONS +VAMPIRIQUES +VAMPIRISEES +VAMPIRISEREZ +VAMPIRISMES +VANADIQUES +VANDALISAI +VANDALISERAI +VANDOISES +VANILLAMES +VANILLE +VANILLERAS +VANILLIERS +VANILLONS +VANNAIS +VANNAT +VANNER +VANNERIES +VANNETS +VANNIERES +VANTAIENT +VANTARDISES +VANTAUX +VANTERAIENT +VANTERONS +VANUATUANS +VAPORETTOS +VAPORISAIS +VAPORISAT +VAPORISER +VAQUAIENT +VAQUASSIONS +VAQUERAIT +VAQUES +VARANGUE +VARAPPAS +VARAPPER +VARAPPERIONS +VARAPPIONS +VARIAIS +VARIASSE +VARIATIONS +VARIEE +VARIERENT +VARIETAUX +VARIOLEES +VARIOMETRES +VARLOPA +VARLOPASSE +VARLOPENT +VARLOPERIEZ +VARMETRES +VARONNES +VARS +VASAI +VASAS +VASECTOMISA +VASECTOMISAT +VASELINANT +VASELINEE +VASELINERENT +VASELINONS +VASEREZ +VASIERE +VASOMOTEUR +VASOUILLAMES +VASOUILLEZ +VASSALIQUES +VASSALISEES +VASSALISEREZ +VASSALITE +VASTITUDE +VATICINANT +VATICINES +VAUCLUSIEN +VAUDOUS +VAUDRONS +VAUTRAI +VAUTRASSES +VAUTRERA +VAUTRERONS +VAVASSAUX +VECTORIELLE +VECUMES +VEDA +VEDETTISERA +VEDIQUES +VEGETAIS +VEGETALISAI +VEGETALISERA +VEGETAMES +VEGETASSENT +VEGETATIVE +VEGETERAIT +VEGETIEZ +VEHICULA +VEHICULASSE +VEHICULENT +VEHICULERIEZ +VEHICULONS +VEILLAS +VEILLEES +VEILLEREZ +VEILLEZ +VEINAIT +VEINASSES +VEINERA +VEINERONS +VEINIONS +VELA +VELANI +VELASSE +VELCROS +VELENT +VELERIEZ +VELIDELTISTE +VELLAVE +VELOCEMENT +VELOCIRAPTOR +VELOMOTEURS +VELOSKIS +VELOUTANT +VELOUTEE +VELOUTERAIT +VELOUTEUSES +VELTAGE +VELUX +VENAISSINE +VENANT +VENDAIS +VENDANGEAS +VENDANGENT +VENDANGERAIT +VENDANGES +VENDANT +VENDERESSE +VENDIEZ +VENDIT +VENDRAS +VENDU +VENENOSITE +VENERAMES +VENERATION +VENERERAI +VENERERONT +VENERIENNES +VENES +VENEZUELIEN +VENGEAMES +VENGEAT +VENGERAIS +VENGERONS +VENIELLE +VENIN +VENTA +VENTEAU +VENTEUX +VENTILASSENT +VENTILATOIRE +VENTILERAIS +VENTILEUSE +VENTOSES +VENTRECHES +VENTRILOQUE +VENTRUES +VENUS +VEPREES +VERANDAS +VERBALISAI +VERBALISE +VERBALISERAS +VERBALISIONS +VERBENACEES +VERDATRE +VERDI +VERDIR +VERDIRIONS +VERDISSANTE +VERDISSIONS +VERDOYA +VERDOYAS +VERDOYERENT +VERDUNISAMES +VERDUNISEZ +VERDURIERE +VERGEES +VERGETE +VERGEURE +VERGLACEE +VERGOBRETS +VERIF +VERIFIAS +VERIFICATIF +VERIFIEES +VERIFIEREZ +VERIFIEZ +VERISMES +VERJUTAI +VERJUTASSIEZ +VERJUTERAI +VERJUTERONT +VERLAN +VERMICELLE +VERMICULITE +VERMIFUGEAI +VERMIFUGES +VERMILLANT +VERMILLENT +VERMILLERIEZ +VERMILLONNAI +VERMISSEAU +VERMOULANT +VERMOULEE +VERMOULERENT +VERMOULONS +VERMOUTS +VERNAUX +VERNIRA +VERNIRONS +VERNISSAMES +VERNISSE +VERNISSERAS +VERNISSEUSE +VEROLAIENT +VEROLASSIONS +VEROLERAIENT +VEROLES +VERONAISES +VERRANNE +VERRES +VERRONS +VERROUILLAIS +VERRUCOSITES +VERSAI +VERSANTES +VERSATILE +VERSEMENTS +VERSEREZ +VERSEUSE +VERSIFIAIT +VERSIFIATES +VERSIFIENT +VERSIFIERIEZ +VERSIONS +VERTE +VERTES +VERTICILLEE +VERTIGO +VERVELLES +VES +VESICANT +VESICULAIRES +VESPA +VESPETRO +VESSAIT +VESSATES +VESSERENT +VESSIEZ +VESTIAIRE +VESTIGIALES +VESULIENNES +VETAIS +VETERANCE +VETEZ +VETILLARDE +VETILLATES +VETILLERENT +VETILLEZ +VETIRAIS +VETISSE +VETONS +VETUSTES +VEULE +VEUX +VEXANTS +VEXATEURS +VEXENT +VEXERIEZ +VEXILLES +VIABILISA +VIABILISENT +VIABILITES +VIANDAI +VIANDASSIEZ +VIANDERAI +VIANDERONT +VIATIQUES +VIBRAI +VIBRAPHONES +VIBRATES +VIBRATOS +VIBRERAIT +VIBREURS +VIBRIONNANT +VIBRIONNAT +VIBRIONNERAS +VIBRIONNIONS +VICARIANT +VICELARDES +VICES +VICHYSSOISE +VICIAIT +VICIATES +VICIENT +VICIERIEZ +VICIIEZ +VICISSITUDES +VICTIMAIRES +VICTIMISAS +VICTIMISE +VICTIMISERAS +VICTIMISIONS +VICTORIA +VICTUAILLE +VIDAMES +VIDANGEANT +VIDANGEES +VIDANGERENT +VIDANGEUSES +VIDASSIONS +VIDELLE +VIDEOCLUB +VIDERAIENT +VIDES +VIDIMAI +VIDIMASSIEZ +VIDIMERAI +VIDIMERONT +VIDONS +VIEILLARDE +VIEILLIE +VIEILLIRENT +VIEILLISSANT +VIEILLISSIEZ +VIELES +VIELLASSENT +VIELLERAI +VIELLERONT +VIELLONS +VIENDRONS +VIENS +VIES +VIGIE +VIGILE +VIGNES +VIGNETAS +VIGNETEES +VIGNETTE +VIGNOT +VIGUERIE +VIL +VILEBREQUINS +VILIPENDAMES +VILIPENDE +VILIPENDERAS +VILIPENDIONS +VILLES +VIMANAS +VINAIGRAIENT +VINAIGRERONS +VINAIGRONS +VINASSES +VINCRISTINES +VINEE +VINERENT +VINEZ +VINICULTURE +VINIFIAMES +VINIFICATEUR +VINIFIER +VINIFIERIONS +VINIQUES +VINSSIEZ +VINYLIQUES +VIOLACA +VIOLACASSES +VIOLACERA +VIOLACERONS +VIOLAIT +VIOLATES +VIOLE +VIOLENTAIS +VIOLENTAT +VIOLENTERAIS +VIOLENTEZ +VIOLERAIT +VIOLETA +VIOLETASSES +VIOLETES +VIOLEUR +VIOLIONS +VIOLONANT +VIOLONCELLES +VIOLONES +VIOQUE +VIPERIDE +VIRAGOS +VIRAILLAS +VIRAILLER +VIRAILLIONS +VIRASSE +VIREES +VIREO +VIREREZ +VIREUR +VIREVOLTAMES +VIREVOLTIEZ +VIRGINITE +VIRGULASSE +VIRGULENT +VIRGULERIEZ +VIRIL +VIRILISANT +VIRILISAT +VIRILISERAI +VIRILISERONT +VIRILOCAL +VIROIDES +VIROLAS +VIROLEES +VIROLEREZ +VIROLIERE +VIROLOGISTES +VIRTUALISERA +VIRULENCES +VIS +VISAIS +VISAT +VISCERES +VISENT +VISERIEZ +VISIBILITE +VISIGOTHES +VISIONNA +VISIONNANT +VISIONNEE +VISIONNERAIT +VISIONNEUSES +VISITANDINE +VISITATES +VISITENT +VISITERIEZ +VISITIEZ +VISONNIERE +VISSAI +VISSASSIEZ +VISSERAI +VISSERIONS +VISTA +VISUALISAMES +VISUALISEZ +VITACEE +VITALITES +VITAMINER +VITAUX +VITESSE +VITILIGO +VITRAGE +VITRANT +VITRE +VITRERAS +VITREUSE +VITRIFIABLE +VITRIFIE +VITRIFIERAS +VITRIFIIONS +VITRIOLAIS +VITRIOLAT +VITRIOLERAIS +VITRIOLEUR +VITRIOLS +VITROPHANIES +VITUPERANT +VITUPERERA +VITUPERERONS +VIVACES +VIVANEAU +VIVARIUMS +VIVEURS +VIVIEZ +VIVIFIANTS +VIVIFIERA +VIVIFIERONS +VIVIPARITE +VIVOTAIS +VIVOTAT +VIVOTERAS +VIVOTIONS +VIVREES +VIVRONT +VOCABLE +VOCALISA +VOCALISASSES +VOCALISERAIT +VOCALISIEZ +VOCATIONNEL +VOCEROS +VOCIFERANTS +VOCIFERERA +VOCIFERERONS +VODKAS +VOGUAI +VOGUASSIEZ +VOGUERAIS +VOGUEZ +VOIEVODE +VOILAIT +VOILATES +VOILERAIENT +VOILERONS +VOILIERS +VOIS +VOISINAGES +VOISINASSES +VOISINES +VOITURAI +VOITURASSIEZ +VOITURERAI +VOITURERONT +VOITURIN +VOIX +VOLAILLER +VOLAIT +VOLAS +VOLATILES +VOLATILISAS +VOLATILISEE +VOLATILITE +VOLCANISAIT +VOLCANISATES +VOLCANISIEZ +VOLER +VOLERIES +VOLETAIS +VOLETASSES +VOLETIONS +VOLETTERAIS +VOLEURS +VOLIGEAGES +VOLIGEASSES +VOLIGERA +VOLIGERONS +VOLITION +VOLLEYAMES +VOLLEYBALL +VOLLEYERAIS +VOLLEYEUR +VOLNAYS +VOLTAGES +VOLTAIRIENNE +VOLTAMPERE +VOLTATES +VOLTERENT +VOLTIGEA +VOLTIGEASSES +VOLTIGERA +VOLTIGERONS +VOLTIONS +VOLUBILITES +VOLUMEN +VOLUMINEUSES +VOLUPTUEUSES +VOLVOCALE +VOMERIENS +VOMIR +VOMIRIONS +VOMISSEMENTS +VOMITES +VORACE +VOSGIENNE +VOTANTE +VOTATES +VOTERAIENT +VOTES +VOTRES +VOUASSENT +VOUDRAIENT +VOUEE +VOUERENT +VOUGEOTS +VOULEZ +VOULURENT +VOUS +VOUSOIERAS +VOUSOYAIS +VOUSOYAT +VOUSOYIONS +VOUSSOIR +VOUSSOYASSE +VOUSSOYER +VOUTAI +VOUTASSENT +VOUTEMENTS +VOUTEREZ +VOUVOIE +VOUVOIEREZ +VOUVOYAIT +VOUVOYATES +VOUVOYONS +VOYAGEAIT +VOYAGEATES +VOYAGERAIT +VOYAGEURS +VOYAIT +VOYERS +VOYIEZ +VRACS +VRAQUIER +VRILLAIT +VRILLATES +VRILLERAIT +VRILLETTES +VROMBIRAIENT +VROMBIS +VROUM +VULCANIENNES +VULCANISASSE +VULCANISEE +VULCANISONS +VULGARISA +VULGARISIEZ +VULNERABLES +VULPINS +VULVITE +WADING +WAGNERISMES +WAGONNIER +WAKEBOARDS +WALLABY +WALLISIENNES +WAOUH +WARRANTA +WARRANTASSE +WARRANTENT +WARRANTERIEZ +WASABI +WASSINGUES +WATERPROOFS +WATTEE +WATTS +WEBCAMERA +WEBMESTRES +WEDGES +WELSCHES +WENZES +WESTIES +WHIPCORD +WHIST +WIGWAMS +WINCHES +WINDSURFERS +WINTERGREEN +WITLOOFS +WOLOFS +WORMIENS +WUS +XANTHIES +XANTHOMES +XENELASIES +XENOGREFFES +XENOPHILIES +XERODERMIE +XEROPHYTE +XIEME +XIPHOIDIENNE +XYLENE +XYLOGRAPHIE +XYLOPHAGIE +XYSTE +YACHTSMANS +YAK +YAKUZA +YANKEE +YAPOCK +YASSAIS +YASSAT +YASSERAS +YASSEUSE +YEARLING +YENS +YESHIVOT +YIDDISH +YODLAIS +YODLAT +YODLERAS +YODLEUSE +YOGHOURTS +YOHIMBINES +YOROUBA +YOUPIE +YOUTSANT +YOUTSENT +YOUTSERIEZ +YOUYOUS +YOUYOUTERAI +YOUYOUTERONT +YOYOTAIT +YOYOTATES +YOYOTERENT +YOYOTONS +YOYOTTASSENT +YOYOTTERAI +YOYOTTERONT +YPREAU +YSOPETS +YTTRIFERES +YUES +YVELINOIS +ZAIBATSU +ZAKATS +ZAMIAS +ZANCLES +ZAOUIAS +ZAPOTEQUES +ZAPPASSENT +ZAPPER +ZAPPERIONS +ZAPPEZ +ZARBIS +ZAZOU +ZEBRANT +ZEBREE +ZEBRERENT +ZEBRONS +ZEFS +ZELATEUR +ZELLIGE +ZENANAS +ZENITHS +ZEPHYRIENNE +ZEROTA +ZEROTASSE +ZEROTENT +ZEROTERIEZ +ZERUMBETS +ZESTASSE +ZESTENT +ZESTERIEZ +ZESTONS +ZEUGMA +ZEZAIERA +ZEZAIERONT +ZEZAYASSE +ZEZAYERA +ZEZAYERONS +ZIBELINES +ZIEUTAMES +ZIEUTE +ZIEUTERAS +ZIEUTIONS +ZIGONNAIS +ZIGONNAT +ZIGONNERAS +ZIGONNIONS +ZIGOUILLES +ZIGZAGUAI +ZIGZAGUERAIS +ZIGZAGUEZ +ZINCAGES +ZINDEROIS +ZINGIBERACEE +ZINGUASSE +ZINGUENT +ZINGUERIE +ZINGUIEZ +ZINZINS +ZINZINULERAI +ZIP +ZIPPASSENT +ZIPPER +ZIPPERIONS +ZIRABLES +ZIRIDE +ZIZIS +ZOANTHROPIES +ZOE +ZOLIENNES +ZONAGES +ZONAMES +ZONASSIEZ +ZONENT +ZONERIEZ +ZONIEZ +ZOOCHORIE +ZOOLITES +ZOOLOGUE +ZOOMAMES +ZOOME +ZOOMERAS +ZOOMIONS +ZOONOSES +ZOOPHILIES +ZOOPSIES +ZOOTHERAPIES +ZOROASTRIENS +ZOU +ZOUKAI +ZOUKASSIEZ +ZOUKERAIS +ZOUKEUR +ZOULOUE +ZOZOTAIS +ZOZOTAT +ZOZOTERAIS +ZOZOTEUR +ZUCHETTE +ZUTIQUE +ZWANZANT +ZWANZENT +ZWANZERIEZ +ZWANZIEZ +ZWINGLIENS +ZYEUTAS +ZYEUTEES +ZYEUTEREZ +ZYGENE +ZYGOMYCETE +ZYGOSPORE +ZYMOLOGIE +abacule +abaissant +abaissat +abaisserai +abaisseront +abajoue +abandonnames +abandonnerez +abasourdimes +abasourdisse +abatage +abatardirai +abatardiront +abatardissez +abattable +abattee +abattez +abattissions +abattrait +abattues +abbaye +abceda +abcedasses +abcedera +abcederons +abdication +abdiquasse +abdiquent +abdiqueriez +abdomens +abductrice +abeillers +abenaquis +aberrance +aberrassiez +aberrerai +aberreront +abetie +abetirent +abetissant +abetissiez +abhorrames +abhorre +abhorreras +abhorrions +abietinee +abimant +abimee +abimerent +abimons +abjecte +abjurait +abjurates +abjurera +abjurerons +ablactations +ablatas +ablatees +ablaterez +ablation +ableret +abnegations +aboierait +aboiteaux +abolirait +abolissais +abolit +abolitive +abominais +abominat +abominerai +abomineront +abondait +abondants +abondee +abonderait +abondiez +abonnas +abonnees +abonneras +abonnie +abonnirait +abonnissais +abonnissions +abordages +abordasses +abordera +aborderons +aborigene +abornasse +abornement +abornerent +abornons +abouchai +abouchassiez +aboucher +aboucherions +aboulaient +aboulassions +abouleraient +aboules +abouta +aboutasse +aboutement +abouterent +abouties +aboutiras +aboutissait +aboutissions +aboyait +aboyates +aboyeuses +abracadabras +abrasasse +abrasent +abraseriez +abrasimetres +abreagimes +abreagiriez +abreagissent +abregeai +abregeassiez +abreger +abregerions +abreuvai +abreuvassiez +abreuver +abreuverions +abreviateur +abriasse +abricotee +abriees +abrierez +abris +abritassent +abriter +abriterions +abrivent +abroge +abrogeas +abrogent +abrogerez +abroutie +abroutirent +abroutissant +abroutit +abrutimes +abrutiriez +abscisse +absentaient +absentent +absenteriez +absidal +absidiole +absolues +absolussiez +absolutoire +absolviez +absorbames +absorbasses +absorbera +absorberons +absorbons +absoudrait +absoute +abstenions +abstenus +abstiendrons +abstinente +abstinssions +abstractive +abstrairais +abstrait +abstrayiez +absurdites +abusasse +abusent +abuseriez +abusiez +abutai +abutassiez +abuterai +abuteront +abyssale +academisme +acagnardes +acalephes +acariens +acatene +accablaient +accablassent +accablements +accablerez +accalmie +accaparantes +accapare +accaparerais +accapareur +accastilles +accedames +accedassions +accederait +accediez +accelerandos +accelerateur +accelerer +accentuables +accentuasses +accentuel +accentuerait +accentuiez +acceptais +acceptasses +acceptent +accepteriez +acceptiez +accessions +accidentas +accidentees +accidentez +accidents +acclamai +acclamassiez +acclame +acclameras +acclamions +acclimatant +acclimates +accointames +accointat +accointerais +accointez +accolaient +accolassions +accolera +accolerons +accombants +accommodants +accommoderez +accompagna +accompagnas +accompagne +accompagniez +accomplis +accons +accoras +accordable +accordant +accordat +accorderez +accordez +accorent +accoreriez +accornees +accosta +accostant +accostee +accosterent +accostons +accotasse +accotement +accoterent +accotoir +accouant +accouchai +accoucher +accouchions +accoudasse +accoudement +accouderent +accoudoir +accoueraient +accoues +accouplais +accouplat +accouplerai +accoupleront +accourci +accourciras +accourcit +accourra +accourront +accourussent +accoutrait +accoutrates +accoutres +accoutumames +accoutumat +accoutumez +accouvait +accouvates +accouverait +accouveurs +accreditais +accreditat +accrediterai +accreditions +accrescent +accretant +accretee +accreterent +accretions +accrochaient +accrocherez +accrochez +accroissant +accroit +accroitrions +accroupirai +accroupiront +accroupissez +accrurent +accueil +accueillera +accueils +acculasse +acculement +acculerent +acculons +acculturasse +acculturee +acculturons +accumule +accumuleras +accumulions +accusames +accusateur +accuse +accuseras +accusions +acensais +acensat +acenserais +acensez +aceracees +acerasses +aceree +acererent +acericulteur +acescence +acetabulum +aceteuse +acetifias +acetifie +acetifieras +acetifiions +acetometre +acetylures +achalandage +achalander +achalantes +achalat +achalerais +achalez +achards +acharnas +acharnees +acharneras +acharnions +acheen +acheminait +acheminates +achemines +achetables +achetasses +achetera +acheterons +achetons +achevais +achevat +acheverai +acheveront +achigans +achoppai +achoppassiez +achopper +achopperions +achromat +achromatisas +achromatises +achylies +acidifiable +acidifiants +acidifierait +acidifiiez +acidiphiles +acidulai +acidulassiez +acidulerai +aciduleront +acierage +acierassent +acierees +aciererez +acierie +acinetien +aclinique +acneiques +acon +acoquina +acoquinasses +acoquinent +acoquineriez +acores +acoumetries +acqueresses +acquerra +acquerront +acquiesces +acquisitif +acquissions +acquittait +acquittates +acquittes +acrete +acrimonie +acrodynie +acromiales +acropoles +acryliques +actancielle +actassions +acteraient +actes +actinides +actiniques +actinometres +actionnable +actionnarial +actionner +actionnismes +activante +activates +activement +activerez +activions +actrices +actualisant +actualisera +actualites +actuations +aculeate +acuponcteurs +acycliques +adagio +adamique +adaptables +adaptasses +adaptations +adaptera +adapterons +addax +addiction +additifs +additionnant +additionnees +additivees +adduits +adenopathies +adequatement +adextree +adheras +adherences +adhererait +adheriez +adhesivites +adiaphoreses +adipolyse +adire +adjacentes +adjectivee +adjectiveras +adjectivions +adjectivisez +adjoignent +adjoignisses +adjoindrait +adjointe +adjugeasse +adjugeons +adjugeriez +adjuraient +adjurassions +adjurera +adjurerons +adjuvants +admettent +admettrait +adminicules +administras +administrer +admirables +admirasses +admirations +admirer +admirerions +admises +admissions +admonestais +admonestat +admonesterai +adnees +adolescences +adonienne +adonnames +adonne +adonneras +adonnions +adoptames +adoptassions +adopteraient +adoptes +adoptifs +adorai +adorassiez +adore +adoreras +adorions +adornassent +adorner +adornerions +adossa +adossasses +adossent +adosseriez +adoubai +adoubassiez +adouber +adouberions +adoucies +adoucirez +adoucissait +adoucisseur +adragantes +adressa +adressant +adressee +adresserent +adressons +adscrit +adsorbant +adsorbat +adsorberais +adsorbez +adulaient +adulasses +adulatrices +adulerait +adulescente +adulteraient +adulterera +adultererons +adulterons +adventice +advenues +adversatifs +adviendront +adynamiques +aegosomes +aerait +aerates +aerent +aereriez +aeriennes +aerobic +aerobique +aerogastries +aerographes +aeromobiles +aeronaute +aeronomie +aerophones +aerosondage +aerotherme +aeschnes +afars +affabulaient +affabulee +affabulerent +affabulons +affadiraient +affadis +affaiblie +affaiblirent +affaiblissez +affairait +affairates +affaires +affaissai +affaisser +affaitages +affaitasses +affaitent +affaiteriez +affalai +affalassiez +affaler +affalerions +affamaient +affamassions +affameraient +affames +affeagea +affeageasses +affeagera +affeagerons +affectai +affectassiez +affecter +affecterions +affectionnai +afferames +affere +affererait +afferiez +affermait +affermataire +affermes +affermirai +affermiront +affermisses +affetee +affichable +affichas +affichees +afficherez +afficheuse +affide +affilait +affilates +affileraient +affiles +affiliant +affiliations +affilierait +affiliiez +affinai +affinassiez +affiner +affineries +affinez +affiquets +affirmassent +affirmation +affirment +affirmeriez +affixale +affleurai +affleurer +affliction +affligeames +affliges +afflouait +afflouates +afflouerait +afflouiez +affluas +affluences +affluerait +affluiez +affolant +affolat +affolerai +affoleront +affouageai +affouagerai +affouilla +affouillent +affouragea +affourager +affourchais +affourchat +affourchez +affranchie +affretant +affretee +affreterait +affreteurs +affriandai +affrianderai +affrichait +affrichates +affricherait +affrichiez +affriolante +affriolates +affriolerait +affrioliez +affrontait +affrontates +affrontes +affruita +affruitasses +affruitera +affruiterons +affublais +affublat +affublerai +affubleront +affutage +affutassent +affuter +affuterions +affutiaux +afghans +afocale +africanisais +africanisez +afrobeat +agacames +agacassions +agacera +agaceriez +agadas +agamide +agapetes +agassin +agatises +agenaises +agencassent +agencements +agencerez +agenciers +agendant +agendee +agenderent +agendons +agenouillat +agenouilles +agentif +aggadah +agglomerames +agglomerates +agglomererai +agglutinai +agglutinasse +agglutinee +agglutinines +aggravames +aggravera +aggraverons +aghlabides +agio +agiotant +agiotent +agioteriez +agiotiez +agirent +agissais +agisses +agitait +agitates +agitee +agiterent +agitons +agnate +agneaux +agnelant +agnelee +agnelerait +agnelets +agnellera +agnelleront +agnostique +agonies +agonirez +agonisait +agonisassiez +agoniserais +agonisez +agonisses +agonites +agrafage +agrafassent +agrafer +agraferions +agraina +agrainasse +agrainent +agraineriez +agraires +agrandi +agrandiras +agrandissait +agrandissiez +agraphiques +agreages +agreasses +agreent +agreeriez +agreg +agregats +agregeasse +agregeons +agregeriez +agreions +agrementasse +agrementent +agreons +agressasse +agressent +agresseriez +agressiez +agressons +agriculture +agriffasse +agriffent +agrifferiez +agriles +agrippais +agrippat +agripperai +agripperont +agrologies +aguerrimes +aguerririez +aguets +aguichait +aguichassiez +aguicherai +aguicheront +aguilla +aguillasses +aguillera +aguillerons +ahana +ahanasses +ahaneraient +ahanes +aheurtait +aheurtates +aheurtes +ahurira +ahurirons +ahurisse +ahurites +aichasse +aichent +aicheriez +aida +aidas +aideaux +aideras +aidions +aieux +aiglonne +aigrelette +aigreur +aigrirai +aigriront +aigrisses +aiguage +aiguilla +aiguillasse +aiguillees +aiguillerez +aiguilletat +aiguilliers +aiguisa +aiguisant +aiguisee +aiguiserait +aiguiseurs +aikido +aileron +aillade +aillassent +ailler +aillerions +ailloli +aimais +aimantant +aimantations +aimanterait +aimantiez +aimat +aimerais +aimez +ainou +airains +airassions +airedales +airerais +airez +aise +aissettes +ajacciennes +ajointant +ajointee +ajointerent +ajointons +ajourait +ajourates +ajourerait +ajouriez +ajournant +ajournee +ajournerait +ajourniez +ajoutait +ajoutates +ajouterait +ajoutiez +ajustaient +ajustassions +ajustera +ajusterons +ajustoir +akans +akinesies +akvavits +alabastrites +alambics +alambiquas +alambiquees +alambiquerez +alandier +alanguirais +alanguissiez +alarmai +alarmasse +alarment +alarmeriez +alarmiste +alaternes +albanophone +albedos +albienne +albites +albugos +albuminemies +alcalescent +alcalimetres +alcalinisat +alcalinises +alcaloide +alcaptone +alcenes +alcides +alcoole +alcooliques +alcoolisas +alcoolise +alcooliseras +alcoolisions +alcootests +alcoyles +alcyons +aldines +ale +alenconnais +alentir +alentirions +alentisses +aleoutes +aleppine +alertames +alerte +alerterait +alertiez +alesait +alesates +aleserait +aleseurs +alesoir +aleurodes +alevinai +alevinassiez +alevinerai +alevineront +alevins +alexithymie +alfa +algal +algebrique +algeroise +alginate +algologues +algonquien +alibis +alicycliques +alienaient +alienassent +alienation +alienerai +alieneront +aliferes +alignas +alignees +aligneras +alignions +alimentaire +alimente +alimenteras +alimentions +aliphatique +alisma +alitaient +alitassions +alitera +aliterons +alizaris +alkekenges +allaient +allaitante +allaitates +allaites +allantes +allas +alle +allechants +allechee +allecherait +allechiez +allegeables +allegeasse +allegements +allegerent +allegies +allegiras +allegissait +allegites +allegorisait +allegorise +allegorisons +allegro +alleguasse +alleguent +allegueriez +alleles +allemands +allergides +alleutier +alliages +alliant +allicines +allierait +alligator +allodial +allogreffe +allongeai +allonger +allongerions +allopathie +allosaure +alloties +allotirez +allotisse +allotites +allouames +allouchier +allouerais +allouez +allumaient +allumassions +allumeraient +allumes +allumeuses +allures +alluviales +alluvionnee +almagestes +almasiliums +almoravides +alopecie +alouchiers +alourdis +alourdissez +alpaga +alpaguait +alpaguates +alpaguerait +alpaguiez +alpha +alphabetisee +alpinisme +alsacien +alterabilite +alterames +alterassions +alterent +altereriez +alterna +alternantes +alternateur +alternees +alternerez +alterons +alti +altimetre +altise +alucites +aluminaire +aluminassiez +aluminera +alumineriez +aluminiages +aluminites +alumnats +alunant +alunee +alunerent +aluni +alunirai +aluniront +alunisses +alus +alvine +alyssums +amadoua +amadouasses +amadouent +amadoueriez +amadouvier +amaigrirai +amaigriront +amalgama +amalgamasses +amalgament +amalgameriez +amancha +amanchasses +amanchera +amancherons +amandee +amanites +amareyeur +amarinages +amarinasses +amarinera +amarinerons +amarniens +amarras +amarrees +amarrerez +amassant +amassee +amasserent +amasseurs +amateurisme +amatira +amatirons +amatissez +amaurotique +amazonien +ambassadeurs +ambiancas +ambiancer +ambiancions +ambifia +ambifiasses +ambifiera +ambifierons +ambiguite +ambitieuse +ambitionnee +ambitions +amblaient +amblassions +amblerait +ambleurs +amblyopies +ambons +ambrassent +ambreines +ambrerez +ambrez +ambrosiaque +ambulance +ambulatoire +ameliorable +ameliorants +ameliorera +ameliorerons +amenagea +amenageasse +amenagements +amenagerent +amenageuses +amenames +amenda +amendasse +amendement +amenderent +amendons +amenerait +ameniez +amensale +amenuisai +amenuiser +amerasienne +americanisa +americanisat +americiums +amerlos +amerriraient +amerris +amerrissez +ametabole +ameublement +ameublirais +ameublissiez +ameulonnames +ameulonne +ameulonneras +ameulonnions +ameutasse +ameutement +ameuterent +ameutons +amiantee +amibiases +amicales +amidon +amidonnas +amidonnees +amidonnerez +amidonniere +amienois +amimique +amincirais +amincissent +aminees +aminoside +amis +amitieuses +ammaniens +ammoniac +ammonitrate +amnesique +amniotiques +amnistiante +amnistiates +amnistierait +amnistiiez +amochas +amochees +amocherez +amodal +amodiant +amodiates +amodient +amodieriez +amoindrie +amoindrirent +amoindrites +amolliraient +amollis +amomes +amoncelerent +amoncellerai +amoncelles +amoralismes +amorcaient +amorcassions +amorceraient +amorces +amorcons +amordancer +amorphes +amortirait +amortisseur +amouilla +amouillasse +amouillera +amouillerons +amouraches +amoureux +ampelite +ampere +amphibioses +amphigenes +amphiphile +amphisbenes +ampholytes +amplective +ampliateurs +amplifiai +amplifiasse +amplifies +ampoule +amputait +amputates +amputeraient +amputes +amuie +amuirent +amuissant +amuit +amurant +amuree +amurerent +amurons +amusants +amusee +amuserait +amusettes +amusons +amygdalines +amylaces +amylobacters +amyotrophies +anabiose +anabolisee +anacoluthes +anacruse +anagallis +anagnostes +analepses +analite +analogismes +analycites +analysante +analysates +analyserait +analyseurs +analyticites +anamorphose +anaphases +anar +anarchisme +anastatique +anastomosa +anastomoses +anastyloses +anathemes +anathemisera +anatocisme +anatomiques +anatomiser +anatoxine +ancestraux +ancien +ancolies +ancrais +ancrat +ancrerais +ancrez +andainai +andainassiez +andainerai +andaineront +andalous +andines +andouillers +androcephale +androgenique +andrologie +andropogon +aneantimes +aneantiriez +anecdote +anecdotisat +anecdotisons +anemiai +anemiasse +anemient +anemieriez +anemiques +anemones +anergisante +anesthesia +anesthesias +anesthesient +anevrismale +anevrysmes +angelique +angelisas +angelisees +angeliserez +angelismes +angiectasie +angiologues +angioscopie +angkoriennes +anglaisames +anglaise +anglaiseras +anglaisions +anglicanes +anglicisait +angliciser +angliciste +anglophobe +angoissait +angoisserai +angoisseront +angons +angraecums +anguilleres +anguillulose +anhelai +anhelassiez +anhelerai +anheleront +anhidrotique +anhydrite +aniciens +anilide +animalerie +animalisais +animalisat +animalisez +animassent +animato +animer +animerions +animistes +anisai +anisasse +anisent +aniseriez +anisiennes +anisons +ankarienne +ankylosames +ankylosera +ankyloserons +annalite +annecienne +annelames +annele +annelides +annelleras +annexa +annexasses +annexera +annexerons +annexions +annihilant +annihilerait +annihiliez +annoncai +annoncassiez +annoncerai +annonceront +annone +annotaient +annotassions +annotee +annoterent +annotons +annualisas +annualise +annualiseras +annualisions +annuites +annulais +annulat +annulees +annulerez +anobie +anoblirai +anobliront +anoblisses +anodine +anodisames +anodisation +anodiserais +anodisez +anomal +anomalons +anomoure +anoniers +anonnassent +anonnements +anonnerez +anons +anonymisait +anonymisates +anonymises +anordimes +anordiriez +anordissent +anorexigene +anormal +anosmiques +ansees +antabuse +antagonisant +antagonisees +antagonisons +antanaclase +antecedence +antediluvien +antenais +antennate +anteposa +anteposasses +anteposera +anteposerons +anteriorites +anthemis +anthocyane +anthozoaire +anthraciteux +anthrisque +anthropisat +anthropises +anthropoide +anthyllide +antiaerienne +anticabreur +antichambres +antichretien +anticipant +anticipat +anticiper +anticodon +anticyclique +antidata +antidatasses +antidatera +antidaterons +antidopages +antidrogue +antifadings +antiforme +antigel +antigreves +antiguerre +antijuif +antimeridien +antimissile +antimonies +antinazisme +antinomiques +antioxydants +antipasti +antiphernal +antipiratage +antipodistes +antiproton +antipubs +antiquaires +antiquite +antiradar +antirouilles +antisemites +antisexiste +antisociales +antisportif +antisudorale +antitache +antitout +antitumoral +antiulcereux +antivitamine +antonomase +antrales +anurique +anxieuse +aorte +aoutai +aoutassiez +aoutent +aouteriez +aoutiens +apagogies +apaisante +apaisates +apaiseraient +apaises +apanageait +apanageates +apanagerait +apanagers +apartheids +apatosaure +aperceptible +apercevable +apercevons +apercevrons +apercumes +aperiodique +apero +apetissaient +apetisses +apeurames +apeure +apeureras +apeurions +aphelies +aphidoides +aphylle +apicoles +apieceurs +apigeonnames +apigeonne +apigeonneras +apigeonnions +apiquaient +apiquassions +apiqueraient +apiques +apitoiera +apitoieront +apitoyasse +apitoyer +aplanie +aplanirent +aplanissant +aplanissions +aplatie +aplatirent +aplatissais +aplatissez +aplats +aplombant +aplombee +aplomberent +aplombons +apocalypse +apocrisiaire +apodide +apogee +apollinienne +apologies +apomorphes +aporetiques +apostais +apostasiames +apostasie +apostasieras +apostasiions +apostats +aposterait +apostes +apostillas +apostillees +apostillerez +apostions +apostrophai +apostrophez +apotheque +appairages +appairasses +appairera +appairerons +appalachiens +apparaisse +apparat +appareillade +appareillas +appareillez +apparentai +apparenter +appariai +appariassiez +apparier +apparierions +apparition +appartenait +appartenir +appartins +apparue +apparut +appatames +appate +appateras +appations +appauvris +appelables +appelas +appelees +appellatifs +appellerais +appels +appendice +appendicules +appendre +appendus +appertisera +appesantimes +appetissante +applaudimes +applaudirent +applicables +applicatives +appliquas +appliquees +appliquerez +appliquions +appointai +appointera +appointerons +appointir +appointisses +appondaient +appondions +appondons +appondrions +apponta +appontasse +appontent +apponteriez +appontons +apportasse +apportent +apporteriez +apportiez +apposant +apposee +apposerent +apposition +appreciais +appreciat +apprecierais +appreciez +apprehendant +apprehendees +apprehensifs +apprenantes +apprendrez +apprennent +appreta +appretasse +appretent +appreteriez +appretiez +apprissent +apprivoisat +apprivoises +approbateurs +approbatrice +approchant +approchat +approcherais +approchez +approfondis +approfondit +appropriasse +appropriee +approprions +approuvas +approuvees +approuverez +appuierais +appuya +appuyasses +appuyes +aprems +apriorite +apteres +aptitudes +apuras +apurees +apureras +apurions +apyrogene +aquafortiste +aquaplane +aquarellant +aquarellee +aquarelliste +aquatintes +aquazoles +aquicultrice +aquilin +arabettes +arabisames +arabisera +arabiserons +arabites +arachide +arachnide +arachnophobe +aragonite +aralia +aramon +aras +arasassent +arasements +araserez +aratoire +arbalete +arbitrages +arbitral +arbitrassiez +arbitrera +arbitrerons +arboraient +arborassions +arboreraient +arbores +arboricole +arborisa +arborisasses +arborisent +arboriseriez +arbouse +arbriers +arbustives +arcane +arcbouta +arcbouter +arceau +archaismes +archeen +archeopteryx +arches +archetypale +archeveques +archidiacone +archiducal +archifavoris +archipleine +archivages +archivasses +archivera +archiverons +archivolte +arconnai +arconnassiez +arconnerai +arconneront +arcure +ardennais +ardillons +ardoisasse +ardoisent +ardoiserie +ardoisieres +areage +areiques +arenaces +areneuses +areographie +areometrique +ares +aretins +argelesienne +argentaient +argentasses +argentent +argenterie +argenteurs +argentine +argents +argilaces +argiope +argonnaise +argotisme +argousins +arguames +argue +argueras +arguions +argumentant +argumente +argumenteras +argumentions +argyries +arhats +aridites +arille +arisait +arisates +ariserait +arisiez +arkose +arlesiennes +armai +armassent +armature +armenienne +armeras +armet +arminianisme +armoires +armoriait +armoriassiez +armoriee +armorierent +armorions +arnaquai +arnaquassiez +arnaquerai +arnaqueront +arnica +aroidees +aromatisa +aromatisas +aromatise +aromatiseras +aromatisions +arpege +arpegeassent +arpeger +arpegerions +arpentages +arpentasses +arpentera +arpenterons +arpentons +arquaient +arquassions +arquebusier +arquerait +arquiez +arrachames +arrache +arracherais +arracheur +arrachons +arraisonnant +arraisonnees +arraisonnons +arrangeant +arrangeat +arrangerai +arrangeront +arrentai +arrentassiez +arrenterai +arrenteront +arrerageais +arrerageat +arreragerais +arreragez +arretaient +arretassions +arreteraient +arretes +arrhenotoque +arrieras +arriere +arriereras +arrierions +arrimant +arrimee +arrimerent +arrimeuses +arrisames +arrise +arriseras +arrisions +arrivant +arrivat +arriverais +arrivez +arrobases +arrogantes +arrogeas +arrogent +arrogerez +arrois +arrondirait +arrondisseur +arrosa +arrosant +arrosee +arroserait +arroseurs +arroyos +arsenicaux +arsins +artemia +arteriel +artesiennes +arthritisme +arthropathie +arthrosique +artiche +articulait +articulates +articulent +articuleriez +articulons +artificieux +artiozoaires +artisanes +artocarpe +arvale +aryanisaient +aryanisera +aryaniserons +aryens +asados +ascarides +ascendantes +ascensionnee +ascete +ascitique +ascorbiques +asemanticite +aseptisaient +aseptisera +aseptiserons +asexue +ashanties +asiadollars +asics +asinien +asnieroise +aspartiques +aspergeai +aspergerai +aspergerions +aspergillus +asperseur +asphaltage +asphalter +asphaltiques +asphyxiaient +asphyxier +aspi +aspirail +aspirassent +aspiration +aspiree +aspirerent +aspirines +asque +assagirai +assagiront +assagisses +assaillait +assaillie +assaillirait +assainimes +assainiriez +assainit +assamaise +assarmentant +assarmentees +assassina +assassiner +assauts +assechait +assechates +asseches +assemblasses +assemblent +assembleriez +assembliez +assenas +assenees +assenerez +assentiment +assermentas +assermentee +assertif +asservies +asservirez +assesseure +assettes +asseyions +assibilas +assibile +assibileras +assibilions +assieds +assiegeantes +assiegee +assiegeras +assiegions +assierons +assignaient +assigner +assignerions +assimilable +assimilants +assimilent +assimileriez +assis +assistai +assistante +assistates +assisterait +assistiez +associait +associates +associee +associerent +associons +assoiffant +assoiffee +assoifferent +assoiffons +assoirons +assolant +assolee +assolerait +assoliez +assombris +assommames +assommes +assommons +assonaient +assonantes +assone +assonerez +assorti +assortirais +assortissons +assoupis +assouplie +assouplirent +assourdies +assourdirez +assouvis +assouvissez +assoyez +assujettir +assumaient +assumassions +assumeraient +assumes +assurai +assuranciels +assurassiez +assurera +assurerons +assyrienne +astacicoles +astarte +asteracees +asterisques +asticotaient +asticotes +astics +astiquage +astiquassent +astiquer +astiquerions +astragales +astraux +astreignent +astreindrait +astreinte +astringentes +astrocytome +astrologique +astronome +asturien +ataca +atavisme +ateles +atemporelle +atermoient +atermoierons +atermoyas +atermoyees +athanor +athenee +athetoses +athrepsie +athyroidie +atlanthrope +atlas +atocas +atomicites +atomisas +atomise +atomiseras +atomisez +atonal +atones +atours +atrazine +atroce +atrophiames +atrophies +atropiniques +attablant +attablee +attablerent +attablons +attachants +attachee +attacherait +attachiez +attaquaient +attaquassent +attaquer +attaquerions +attardaient +attardes +atteignant +atteignisse +atteins +attelai +attelassiez +attelet +attellerais +attelloires +attend +attendimes +attendites +attendri +attendrirais +attendrisses +attendront +attentait +attentates +attenterais +attentez +attentives +attenuantes +attenuateur +attenuerai +attenueront +atterraient +atterrassent +atterrements +atterrerez +atterriez +atterrirent +atterrissais +atterrissez +attestais +attestat +attesterai +attesteront +attiedi +attiediras +attiedissait +attiedissons +attifais +attifat +attiferai +attiferont +attigeai +attigeassiez +attigerai +attigeront +attikameques +attirais +attirasse +attirent +attireriez +attisai +attisassiez +attiser +attiserions +attitra +attitrasses +attitrera +attitrerons +attitudinale +attractifs +attraient +attrairiez +attrapade +attrapas +attrapees +attraperez +attrapez +attrayants +attrempais +attrempat +attremperais +attrempez +attribuait +attribuates +attribuerait +attribuiez +attributive +attriquas +attriquees +attriquerez +attrista +attristas +attristees +attristerez +attrition +attroupasse +attroupement +attrouperent +attroupons +aubage +auberes +auberons +aubinai +aubinassiez +aubinerai +aubineront +aubrac +auburniens +audacieuse +audiencaient +audiencera +audiencerons +audiencons +audimutites +audioguidage +audioguidera +audiometre +audiophone +auditaient +auditassions +auditeraient +audites +auditionnat +auditionniez +auditorats +audoises +augees +augite +augmentames +augmentateur +augmente +augmenteras +augmentions +augurales +augurates +augureraient +augures +augustes +augustins +aulnaisienne +aumaille +aumusse +aunait +aunates +aunerait +auniez +aura +aurelie +aureolant +aureolee +aureolerent +aureolons +auricules +aurifiant +aurifierait +aurifiiez +aurillacoise +auroral +ausculta +auscultasses +ausculterait +auscultiez +austenites +australes +austraux +autarcique +authentifia +authentifiat +authentiques +autistiques +autoanalysee +autoantigene +autobloquant +autobus +autocentrees +autochtone +autoclavage +autocopie +autocrate +autodefenses +autodetruite +autodidaxies +autodromes +autofinance +autogamie +autogerait +autogerates +autogererait +autogeriez +autographes +autoguidages +automaticien +automatiques +automatisent +automediquez +automne +automutilai +automutilera +autonomisait +autonomisiez +autonymies +autophagies +autoponts +autoportrait +autoproduis +autopsiasse +autopsient +autopsieriez +autopunitifs +autoregulas +autoreguliez +autorisames +autorisation +autoriserais +autorisez +autosomique +autotractee +autotrophie +autovaccins +autrichiens +auvent +auxdits +auxiliateurs +avachie +avachirent +avachissant +avachit +avalaison +avalante +avalates +avaleraient +avales +avalisaient +avalises +avalistes +avancais +avancat +avancerai +avanceront +avantagea +avantagera +avantagerons +avants +avariant +avarices +avarierai +avarieront +aveline +avenements +aventurait +aventurates +aventurerait +aventurines +avenus +averassent +averer +avererions +averroiste +avertimes +avertiriez +avertissions +aveuglais +aveuglasses +aveuglent +aveugleriez +aveuli +aveuliras +aveulissait +aveulissons +aviateur +avicule +avides +avignonnais +aviliraient +avilis +avinage +avinassent +aviner +avinerions +avioniques +aviron +avironnerai +avironneront +avirulents +avisasse +avisent +aviseriez +aviso +avitaillas +avitaillees +avitailleras +avitaillez +avivaient +avivassions +avivera +aviverons +avocassai +avocasserais +avocasseront +avocat +avoie +avoierions +avoinant +avoinee +avoinerent +avoinons +avoisinames +avoisines +avortait +avortates +avorteraient +avortes +avoua +avouasse +avouent +avoueriez +avoyai +avoyassiez +avoyez +avunculat +axais +axat +axeniques +axeras +axez +axiologie +axiomatisait +axiomatisiez +axonge +ayatollah +ayurvedas +azimutaux +azolla +azoraient +azorassions +azoreraient +azores +azotames +azotates +azotera +azoterons +azotions +azoturie +azur +azurante +azurates +azurera +azurerons +azygos +baballes +babeurre +babillaient +babillasse +babillent +babilleriez +babine +babolaient +babolassions +babolerait +baboliez +babouchka +babouvisme +babysitter +baccaras +bacciforme +bachait +bachates +bachelors +bacherez +bachiques +bachotage +bachotassent +bachoter +bachoterions +bachotions +bacilliferes +backgammons +baclai +baclassiez +baclerai +bacleront +bacon +bactericides +bacteriennes +baculas +badamiers +badaud +badaudassent +badauderai +badauderions +badeche +badent +baderiez +badgeai +badgeassiez +badgerai +badgeront +badigeon +badigeonnas +badigeonnent +badinais +badinat +badineras +badinez +badoise +baffa +baffasses +baffera +bafferons +bafouai +bafouassiez +bafouerai +bafoueront +bafouillait +bafouillates +bafouilleurs +bafra +bafrasses +bafrera +bafrerons +bafrons +bagarraient +bagarres +bagasses +bagdadienne +bagnard +bagouse +baguaient +baguassions +baguenaudat +baguenaudes +baguerai +bagueront +baguio +bahameenne +bahreinienne +bahuts +baierez +baignaient +baignassions +baigneraient +baignes +baignons +baillant +baillee +baillerait +bailles +baillies +baillonnais +baillonnat +baillonnerai +bains +baisables +baisasses +baisement +baiserent +baiseuse +baisotaient +baisotes +baissames +baisse +baisseras +baissiere +bajociennes +bakeofe +baladais +baladat +baladerais +baladeur +baladions +balafrais +balafrat +balafrerais +balafrez +balaierais +balaise +balancames +balance +balancerai +balanceront +balancoires +balanoglosse +balayage +balayassent +balayer +balayerions +balayez +balbutiais +balbutiasses +balbutient +balbutieriez +balbuzards +balcons +baleineaux +balenide +balevres +balisaient +balisassions +baliseraient +balises +balisons +balistites +balivas +baliveaux +baliveras +balivez +balkanisait +balkanisates +balkanises +ballais +ballasses +ballastait +ballastates +ballasterait +ballastiere +baller +ballerine +ballettomane +ballonnait +ballonnates +ballonnes +ballotes +ballottaient +ballottera +ballotterons +ballounes +balneos +balourd +bals +balsas +balubas +balzacienne +bamba +bambochades +bambochards +bambochent +bambocheriez +bambochiez +banal +banalisant +banaliserait +banalisiez +bananait +bananates +bananeraies +bananes +banastes +bancaires +bancarisames +bancarisez +banchages +banchasses +banchera +bancherons +bancoulier +bandagistes +bandantes +bande +banderai +bandez +bandits +bandouliere +bangkokiens +banguissoise +banjulaise +banne +banniere +banniras +bannissaient +bannissiez +banquai +banquassiez +banquerai +banqueront +banquetai +banqueteuses +banquez +banquistes +bantus +baoules +baptisas +baptisees +baptiserez +baptismale +baptisteres +baquasse +baquent +baqueriez +baquetait +baquetates +baquetons +baquetterez +baquons +baragouinait +baragouine +baraquaient +baraquera +baraquerons +baratina +baratinasses +baratinera +baratinerons +baratinons +barattant +barattee +baratterent +barattons +barbai +barbaques +barbarisa +barbarisera +barbasse +barbecue +barbent +barberiez +barbiche +barbieres +barbifiante +barbifiates +barbifierait +barbifiiez +barbiturique +barbotage +barbotassent +barboter +barboterions +barbotieres +barbottes +barbouillas +barbouillent +barbudiennes +barcasses +bardages +bardassa +bardassasses +bardassera +bardasserons +bardeau +barderaient +bardes +bardot +baremiques +baretas +bareter +bareterions +barguigna +barguignasse +barguignera +barguignons +bariola +bariolasse +bariolent +barioleriez +bariolures +barjaquames +barjaque +barjaquerez +barjo +barlotiere +barnache +barodets +baronnial +baroqueuse +baroscopes +baroudant +baroudent +barouderiez +baroudiez +baroula +baroulasses +baroulera +baroulerons +barques +barrai +barrassent +barrees +barrent +barreriez +barrettes +barricadais +barricadat +barricadez +barrions +barrirent +barrissant +barrit +barsacs +bartonienne +barulas +barulees +barulerez +barycentre +baryoniques +barytite +bas +basaltes +basanant +basanee +basanerent +basanite +basat +basculames +basculera +basculerons +baseball +basent +baseriez +basics +basilaire +basiliques +basir +basirions +basisse +basites +basmatis +basophile +basquets +basset +bassier +bassinais +bassinasses +bassinera +bassinerons +bassinoire +basta +bastant +bastates +basterent +bastiais +bastillees +bastion +bastionner +bastonna +bastonnasse +bastonnent +bastonneriez +bastos +bataillai +bataillerais +batailleur +batait +batards +bataves +bate +batelais +batelat +bateleur +batelions +batellerie +batera +baterons +bathoniens +batidas +batifolaient +batifolerait +batifoleurs +batillages +batirait +batissables +batisseuse +batneen +batoillant +batoillent +batoilleriez +batonna +batonnant +batonne +batonneras +batonnez +batons +battaient +battee +batteur +battisse +battoirs +battre +battures +baudet +baugea +baugeasses +baugera +baugerons +baume +bavai +bavardai +bavardassiez +bavarderais +bavardez +bavassai +bavassassiez +bavasserais +bavassez +baver +baverions +baveux +bavochant +bavochee +bavocherent +bavochons +baya +bayasse +bayera +bayerons +bayonnaises +bazadaises +bazardasse +bazardent +bazarderiez +bazaris +beagle +beante +beassiez +beatifiait +beatifiates +beatifies +beatniks +beauforts +bebite +becasseau +becha +bechant +bechee +becherent +becheuse +bechevetasse +bechevetent +bechevettent +becosses +becotasse +becotent +becoteriez +becquee +becquetais +becqueterai +becqueteront +becquetterai +becquettes +bectant +bectee +becterent +bectons +bedeiste +bedonnaient +bedonnassent +bedonnerai +bedonneront +bedouins +beeraient +bees +begaiements +begaierions +begayait +begayassiez +begayer +begayerions +begayions +beguards +beguetas +beguetements +begueterez +begueule +begum +beigeatres +beirams +belait +belassent +belees +belerais +belette +belgeoisant +belier +belinos +bellatre +belluaires +beloteuses +bemol +bemoliser +benard +benedictine +beneficiers +benets +bengalis +benignites +benira +benirons +benisseur +benites +benjoin +benthique +benzenes +benzols +benzyles +beotiennes +bequetais +bequetat +bequeterais +bequetez +bequetterais +bequillage +bequillardes +bequille +bequilleras +bequillions +berberis +bercail +bercassent +bercees +bercerais +berceur +berdines +bergamotier +bergerette +bergsonien +berk +berlinettes +bermuda +bernacles +bernardins +bernee +bernerait +berneurs +bernois +bers +beryl +besacier +beset +besognames +besogne +besogneras +besogneux +bessonne +bestiales +betatron +betifiai +betifiasse +betifient +betifieriez +betisai +betisassiez +betiserais +betisez +beton +betonnas +betonnees +betonnerez +betonnez +betteravier +betulinees +beuglante +beuglates +beugleraient +beugles +beugnames +beugne +beugneras +beugnions +beurrages +beurrasses +beurrera +beurreriez +beurriez +bey +beyrouthine +biacuminee +biaisais +biaisat +biaiserai +biaiseront +biathlete +biaurales +bibande +bibelotai +bibeloterais +bibeloteur +bibendums +biberonnasse +biberonnent +bibi +bibitte +bibliologie +bibliophile +biblique +bicephales +bichant +bichelamar +bicherent +bichiez +bichlorures +bichonnait +bichonnates +bichonnerait +bichonniez +bicipital +biconcave +bicots +bide +bidonnait +bidonnassiez +bidonnerai +bidonneront +bidons +bidouillames +bidouille +bidouilleras +bidouilleuse +biefs +bienfacture +bienfaiteurs +biennaux +bienseants +bievre +biffages +biffasses +biffent +bifferiez +biffins +bifleche +bifoliole +bifurqua +bifurquasses +bifurques +bigarades +bigarras +bigarrerais +bigarrez +bigemines +biglant +biglee +biglerent +biglez +bignonia +bigophonant +bigophonee +bigophonons +bigornant +bigorneau +bigornerait +bigorniez +bigotismes +bigourdanes +bigues +bijectifs +bijoutieres +bilabial +bilais +bilasse +bilees +bilerez +bilharzia +biliees +bilingue +biliverdines +billant +billates +billera +billerons +billetterie +billions +billonnant +billonnee +billonnerent +billonnons +bilocale +biloquaient +biloques +bimbelotier +bimensuels +bimetallisme +bimodaux +binai +binart +binational +binera +bineriez +bineuses +binoclarde +binomiale +bintjes +biochimies +biodegradait +biodegradiez +biodynamies +bioethique +biologie +biologisames +biologises +biomateriaux +biomes +biongules +biophysiques +biopoles +biorythmes +biostasie +biotechnique +biotherapies +bioxydes +bipant +bipartisans +bipassaient +bipasses +bipedie +bipera +biperons +biphases +biplans +bipolarises +bips +birdies +biremes +biroutes +bisaieuls +bisant +bisbilles +biscayens +biscoteaux +biscuita +biscuitasses +biscuitera +biscuiteriez +biscuitons +biseautait +biseautates +biseauterait +biseautiez +biseraient +bises +bisexuel +bisions +bismuthine +bisontine +bisquais +bisquat +bisqueras +bisquine +bissait +bissasse +bissection +bissera +bisserons +bissextile +bissexuelles +bistortes +bistournai +bistournerai +bistrait +bistrates +bistrerait +bistriez +bistrotier +bisulfures +bitangentes +bitates +bitent +biteriez +bitions +bitords +bittas +bittees +bitterez +bittons +bitturant +bitturee +bitturerent +bitturons +bitumai +bitumassiez +bitumerai +bitumeront +bituminais +bituminat +bituminerais +bitumineuse +biturai +biturassiez +biturer +biturerions +biunivoques +biveaux +bivouaquai +bivouaquez +bizarrerie +bizets +bizous +bizutant +bizutee +bizuterent +bizuteuses +blablablas +blablatasse +blablatera +blablaterons +blackboules +blacklistee +blacks +blaguais +blaguat +blaguerais +blagueur +blairai +blairassiez +blairer +blairerions +blaisoise +blamait +blamates +blamerait +blamiez +blanchet +blanchir +blanchirions +blanchisseur +blanchon +blanquistes +blasasse +blasement +blaserent +blason +blasonner +blasphemai +blaspheme +blasphemeras +blasphemions +blastomeres +blatera +blaterasses +blateres +blazer +bledards +blemirai +blemiront +blemissement +blende +blepharite +blesait +blesates +bleserait +blesiez +blessaient +blessassent +blesser +blesserions +blet +bletsasse +bletsent +bletseriez +blettes +blettirait +blettissais +blettissions +bleue +bleui +bleuiras +bleuissaient +bleuissent +bleusailles +bleutassent +bleuter +bleuterions +bliaut +blindages +blindasses +blindera +blinderons +blinqua +blinquas +blinquees +blinquerez +blister +blisterisera +blizzard +block +blogs +bloguassent +bloguerai +blogueront +blond +blondie +blondins +blondiriez +blondissent +blondoiera +blondoieront +blondoyasse +blondoyez +bloqua +bloquas +bloquees +bloquerez +bloquez +blotties +blottirez +blottisse +blottites +blousants +blousee +blouserent +blouson +bluetooths +bluffames +bluffassions +blufferaient +bluffes +blush +blutames +blute +bluteras +blutez +bobards +bobeur +bobinait +bobinassions +bobinera +bobinerons +bobinier +bobinots +bobsleighs +bocaine +bocardais +bocardat +bocarderais +bocardez +bock +bodyboard +boeings +boeufs +bogheads +bogomiles +boguames +bogue +bogueras +boguez +bohrium +boira +boirions +boisait +boisates +boiseraient +boiserons +boisseaux +boita +boitasses +boitera +boiteriez +boitiers +boitillantes +boitille +boitilleras +boitillions +bokit +bolchevistes +boleros +bolivares +bollard +bols +bombaient +bombardais +bombardat +bombarderai +bombarderont +bombasin +bombee +bomberait +bombeurs +bombons +bonamias +bonasserie +bonbons +bondant +bondee +bonderait +bonderisait +bonderisates +bonderises +bondieusard +bondira +bondirons +bondisse +bondites +bondonnasse +bondonnent +bondonneriez +bondree +bonheurs +bonichon +bonifiames +bonification +bonifierais +bonifiez +bonimentait +bonimentates +bonites +bonnet +bonnetiers +bonnottes +bontes +boogie +booleen +boomerang +boostant +boostee +boosterent +boostions +bora +boranes +borazon +bordai +bordassiez +bordelaise +bordelisant +bordelisee +bordelisons +borderait +borderline +bordiez +borduraient +bordures +boreales +borin +bornages +bornasses +bornera +bornerons +bornoiera +bornoieront +bornoyas +bornoyees +borosilicate +borrelia +bortsch +bosco +bosniaque +boss +bossas +bossees +bosselas +bosselees +bossellent +bossellerons +bosseraient +bosses +bossoir +bossuant +bossuee +bossuerent +bossuons +bostonnais +bostonnat +bostonneras +bostonnions +botanisaient +botaniserait +botanisiez +bots +bottant +bottee +bottelant +bottelee +botteliez +bottellerez +botterai +botterions +bottiers +botulinique +boubou +bouboulerai +boubouleront +boucan +boucanas +boucanees +boucanerez +boucanions +bouchage +boucharda +bouchardera +bouchasses +bouchera +boucherie +boucheurs +boucholeur +bouchonnames +bouchonne +bouchonnez +bouchoteur +bouchoyas +bouchoyees +boucla +bouclasse +bouclement +bouclerent +bouclier +boudaient +boudassions +bouddhistes +bouderait +boudes +boudinage +boudinassent +boudiner +boudinerions +boudins +bouela +bouelasses +boueleraient +boueles +boueuse +bouffante +bouffassions +boufferaient +bouffes +bouffi +bouffirais +bouffissage +bouffissiez +bougeasse +bougeoir +bougerait +bougie +bougnat +bougonnames +bougonne +bougonnerais +bougonneront +bougonnons +bougrines +bouillait +bouillee +bouillez +bouillis +bouilloire +bouillonne +bouillonniez +bouillottant +bouillotter +bouinais +bouinat +bouinerais +bouinez +boula +boulangeai +boulangerai +boulangeries +boulangisme +boulasses +bouldozeurs +bouleguaient +boulegues +boulerais +boulet +bouleuse +bouleversai +boulgour +bouline +boulisme +boulochais +boulochat +boulocheras +boulochions +boulonnage +boulonnas +boulonnees +boulonnerez +boulonnions +boulottames +boulotte +boulotteras +boulottions +boumerang +bouquetier +bouquinaient +bouquinerons +bouquiniste +bourbeuses +bourboniens +bourdaloue +bourdonner +bourge +bourgeoisies +bourgeonnant +bourgs +bouriates +bourlinguas +bourlinguera +bouronna +bouronnasses +bouronnes +bourrage +bourrasques +bourrasser +bourratif +bourrelai +bourrelerent +bourrelions +bourrellerie +bourrera +bourrerons +bourriche +bourriez +bourroir +boursette +boursicotant +boursicoter +boursoufflez +boursouflat +boursoufles +bousaient +bousassions +bousculais +bousculat +bousculerais +bousculez +bouserai +bouseront +bousillage +bousiller +bousillions +boustifailla +boustifaille +boutais +boutassent +boutefeu +boutent +bouteriez +bouteurs +boutiquieres +boutonnages +boutonnasses +boutonnent +boutonneriez +boutonniere +boutura +bouturasse +bouturent +boutureriez +bouveries +bouvetant +bouvetee +bouvetons +bouvetterez +bouvillon +bouzy +bowal +boxais +boxat +boxerais +boxes +boy +boyauta +boyautasses +boyautera +boyauterons +boycottage +boycotter +boycottions +brabanconne +brachiale +brachylogies +braconnames +braconne +braconneras +braconniere +bractee +bradait +bradates +braderaient +braderons +bradons +bradypes +bragues +brahmans +braieraient +brailla +braillards +braillee +braillerait +brailleurs +brairions +braisait +braisates +braiserait +braisettes +bramaient +bramassions +bramera +bramerons +brancardage +brancarder +brancardions +branchames +branche +brancherais +branchette +branchions +brandades +brandillames +brandille +brandilleras +brandillions +brandirent +brandissant +brandon +branlames +branlassions +branlera +branlerons +branlez +brantes +braquas +braquees +braquerais +braquet +bras +brasas +brasees +braserez +brasiers +brasillait +brasillates +brasillerait +brasilliez +brasquames +brasque +brasqueras +brasquions +brassant +brassates +brasserait +brasses +brasseyames +brasseye +brasseyeras +brasseyions +brassieres +bravait +bravates +braverais +braveront +bravoure +brayasse +brayent +brayeriez +brayons +breakames +breakdance +breakerais +breakes +brebis +bredins +bredouille +breeder +breitschwanz +brelans +brele +breleras +brelions +bresaola +bresillai +bresillerai +bresilleront +bressane +bretaillait +bretaillates +bretailliez +bretaudas +bretaudees +bretauderez +breteche +bretonnante +brettait +brettates +brettelant +brettelee +brettellera +bretteras +bretteuse +breuvage +brevetage +brevetassent +breveter +breveterions +brevets +brevetteriez +brevites +brichetons +bricolames +bricole +bricoleras +bricoleuse +brida +bridasse +brident +brideriez +bridgeais +bridgeat +bridgerais +bridgeur +bridons +briefas +briefees +brieferez +briefions +briffais +briffat +brifferais +briffez +brigadiste +brigandames +brigande +briganderas +brigandine +brightiques +briguames +brigue +brigueras +briguions +brillamment +brillantait +brillantates +brillanteurs +brillantinas +brillantines +brillas +briller +brillerions +brimades +brimasses +brimbalames +brimbalera +brimbalerons +brimborions +brimerait +brimiez +brinell +bringuaient +bringuebalee +bringuee +bringuerent +bringuons +briochee +briouate +briquasse +briquent +briqueriez +briquetaient +briquetes +briquette +brisai +brisants +briscards +briseraient +brises +briskas +bristol +brivistes +brocantait +brocantates +brocanterait +brocanteurs +brocardaient +brocardes +broccio +brochait +brochassiez +brocherai +brocheront +brocheuse +brocoli +brodas +brodees +broderas +brodeur +broiements +broierions +bromate +bromhydrique +bromothymol +bronchaient +bronches +bronchique +bronzaient +bronzassent +bronzer +bronzerions +bronzez +broquard +brossages +brossasses +brossera +brosseriez +brossiez +brouettages +brouettasses +brouettera +brouetterons +brouettons +brouillais +brouillat +brouillerai +brouillonna +brouillonnat +brout +broutard +broutat +brouterai +brouteront +brownien +broyai +broyassiez +broyes +bru +bruche +brugnonier +bruine +bruirai +bruirons +bruissames +bruisses +bruitaient +bruitassions +bruiteraient +bruites +bruitons +brulant +brulat +brulerai +brulerions +brulions +bruma +brumasserait +brumeux +brumisassent +brumise +brumiseras +brumisions +brunchaient +bruncherait +brunchiez +brunelles +brunir +brunirions +brunisse +brunissiez +brunoises +brusquais +brusquat +brusquerons +brutalement +brutalisasse +brutalisee +brutalisme +bruts +bruyante +bryones +buandieres +bucaille +buccinateur +buchames +buche +bucheras +bucheronnat +bucheronnons +bucheuses +budapestoise +budgetaires +budgetes +budgetisant +budgetisiez +bues +buffets +bufflas +bufflees +bufflerez +buffletin +bufonides +buggant +buggee +buggerent +buggions +bugnaient +bugnassions +bugneraient +bugnes +buire +buissonnerai +buissonniez +bukaviennes +bulbiculteur +bulbuls +bullaient +bullasses +bullee +bullerent +bulleuses +bulots +bunkerisa +bunkerisera +buns +buquas +buquees +buquerez +buraliste +burelle +burgaux +burgraves +burinais +burinat +burinerais +burineur +burkas +burle +burnee +burseracee +busaient +busasse +busee +buserent +bush +businessman +busquaient +busquassions +busqueraient +busques +bussiez +butadiene +butaniers +butat +buterai +buteront +butinage +butinassent +butiner +butinerions +butinions +butomes +buttages +buttasses +buttera +butterons +buttons +butyreuses +buvable +buvees +buveuses +buzza +buzzasses +buzzera +buzzerons +bylines +byzantin +cabalait +cabalates +cabalerait +cabaleurs +cabalons +cabanasse +cabanement +cabanerent +cabanon +cabasset +cabiai +cabinet +cablaient +cablassions +cablera +cableriez +cablier +cablots +cabochons +cabossassent +cabosser +cabosserions +cabotage +cabotassent +caboterai +caboteront +cabotinai +cabotinerais +cabotinez +caboulot +cabrant +cabree +cabrerait +cabrettes +cabriolant +cabriolent +cabrioleriez +cabriolons +cacabai +cacabassiez +cacaberais +cacabez +cacao +cacaoui +cacardait +cacardates +cacarderent +cacardons +cachait +cachassions +cachent +cacherez +cachetai +cachetassiez +cachetez +cachetonnait +cachetonne +cachets +cachetteriez +cachons +cachous +cacodyles +cacologies +cactacees +cadastra +cadastras +cadastree +cadastrerent +cadastrons +caddy +cadeautant +cadeautee +cadeauterent +cadeautons +cadenassames +cadenasse +cadenasseras +cadenassions +cadencasse +cadencent +cadenceriez +cadenes +cadienne +cadmiages +cadmiasses +cadmiera +cadmierons +cadogans +cadrames +cadrat +cadrent +cadreriez +cadriez +caducite +caecale +caesalpiniee +cafardaient +cafardes +cafardises +cafeier +cafes +cafetas +cafetees +cafeterez +cafeteuse +cafouilla +cafouillasse +cafouillera +cafouillions +caftames +caftat +cafterais +cafteur +cage +cagette +cagne +cagoteries +cagoulardes +caguais +caguat +cagueras +caguions +cahotait +cahotassiez +cahoter +cahoterions +cahotons +caieu +caillait +caillassai +caillasserai +caillebotta +caillebottat +cailleraient +cailles +cailletas +cailleteaux +cailletterai +caillettes +cailloutages +cailloutera +cailloutons +cairn +caissier +cajeput +cajolait +cajolates +cajolerait +cajoles +cajous +caktis +calabriennes +calages +calait +calament +calaminais +calaminat +calaminerais +calaminez +calamistrant +calamistrees +calamites +calanchames +calanche +calancherez +calandra +calandrasse +calandrent +calandreriez +calandriez +calassent +calcaire +calcareux +calceolaire +calcifiais +calcifiat +calcifierai +calcifieront +calcina +calcinasses +calcinent +calcineriez +calcio +calcitonine +calculas +calculatoire +calculerai +calculeront +calculons +calebasses +caleconnade +calefactions +calendes +caler +calerions +caletant +caletee +caleterent +caletons +calfatait +calfatates +calfaterait +calfatiez +calfeutrait +calfeutrates +calfeutres +calibra +calibrasse +calibree +calibrerent +calibreuses +caliciformes +calife +calina +calinasses +caliner +calineries +calinez +call +callassent +caller +callerions +calliez +callovienne +calmaient +calmas +calmees +calmerent +calmimes +calmirez +calmisse +calmodulines +calomniait +calomniates +calomniera +calomnierons +calomnions +calorifugees +calorimetres +calottaient +calottes +calquage +calquassent +calquer +calquerions +calquions +caltas +caltees +calterez +caluge +calugeassent +calugerai +calugeront +calva +calvas +calycanthes +camaieux +camaraderie +camarilla +camba +cambalames +cambale +cambaleras +cambalions +cambat +camberais +cambez +cambismes +cambouis +cambouler +cambrages +cambrasses +cambrent +cambreriez +cambreurs +cambriolai +cambriolerai +cambrions +came +camelides +camelots +camerais +camerent +camerlingat +camescope +camionnais +camionnat +camionnerais +camionnette +camions +camoufla +camouflasse +camouflent +camoufleriez +camouflons +campagnols +campanelles +campaniste +campasse +campee +campera +camperons +camphraient +camphres +campos +cana +canadienne +canaillous +canalisables +canalisasses +canaliserait +canalisiez +canant +canardage +canardassent +canardees +canarderez +canardions +canas +canat +cancanait +cancanates +cancanerent +cancanieres +cancellais +cancellerai +cancelleront +cancereux +cancerisames +cancerisez +cancers +cancroides +candida +candidatas +candidater +candidatures +candira +candirons +candissent +cane +canephore +caneras +canetiere +cange +canicule +canine +canitie +cannabiques +cannais +cannat +cannelai +cannelassiez +cannelez +cannellerais +cannellonis +cannerait +cannetages +canneuses +cannibalisme +cannisses +canoeiste +canonicat +canonisai +canoniser +canonna +canonnant +canonnee +canonnerent +canonnieres +canot +canotames +canote +canoterez +canotez +canouns +cantalien +cantals +cantate +canter +canterions +cantharide +cantinai +cantinassiez +cantinerai +cantineront +cantions +cantonaises +cantonnasse +cantonnement +cantonnerent +cantonnieres +cants +canulants +canulassions +canuleraient +canules +canyon +canzonette +caouas +cap +capacitation +capais +capassent +capeais +capeat +capeeras +capeions +capelans +capele +capeline +capellerait +capeons +caperiez +capetien +capeyai +capeyassiez +capeyerais +capeyez +capillarite +capions +capitale +capitalisant +capitalise +capitalisme +capitane +capiteuse +capitonna +capitonnasse +capitonnent +capitoul +capitulais +capitulards +capitulerent +capitulons +caponnai +caponnassiez +caponnerais +caponnez +caporalisa +caporalises +capota +capotasse +capotent +capoteriez +capouan +capres +caprices +caprique +capronniers +capsides +capsulaire +capsulassiez +capsulerai +capsulerions +capsulites +captait +captassions +captatives +captent +capteriez +captieuses +captivait +captivassiez +captiverai +captiveront +capturai +capturassiez +capturerai +captureront +capuchon +capuchonnera +capucine +capverdiens +caquames +caque +caquerais +caquet +caquetante +caquetates +caqueterait +caqueteurs +caquette +caquions +carabinees +caracals +caracolades +caracolasses +caracoles +caracterises +carafa +carafasse +carafent +caraferiez +caraibe +carambolais +carambolat +carambolez +caramelisat +caramelises +carapace +carapatasse +carapatent +carapateriez +caraques +caravaniere +caravelles +carbochimie +carbonado +carbonatera +carbonee +carbonisai +carbonisees +carboniserez +carbonitrura +carbonitrure +carborane +carbura +carburas +carburation +carburer +carburerions +carcailla +carcailles +carcasses +carcinogene +cardai +cardans +carde +carderas +carderont +cardiales +cardiaux +cardinalices +cardiologues +cardiotomie +carelien +carenaient +carenassions +carencas +carencees +carencerez +carene +careneras +carenions +caressait +caressassiez +caresserai +caresseront +caret +carguai +carguassiez +carguerai +cargueront +cariai +cariasse +caribeen +caricaturais +caricaturera +carie +carieras +carieux +carillonnai +carillonnera +carillons +caris +carlingue +carmagnoles +carmina +carminasses +carminee +carminerent +carminons +carnau +carneaux +carnien +carnifiait +carnifiates +carnifies +carnotsets +carolos +carotenoide +carottages +carottasses +carottera +carotterons +carottieres +carouges +carpellaire +carpettiers +carpocapses +carraient +carrasses +carreautes +carrelaient +carrelets +carrellera +carrelleront +carrerait +carrick +carriez +carroierait +carron +carrossait +carrossates +carrosserait +carrosses +carroyage +carroyassent +carroyerent +cars +cartaierais +cartait +cartates +cartayassent +cartayerai +cartayeront +cartelette +cartellisees +cartent +carterie +cartesien +carthame +cartions +cartonna +cartonnasse +cartonnent +cartonnerie +cartonneuses +cartons +cartophilies +cartouchames +carva +caryocineses +caryophyllee +casablancais +casames +casaquin +casbah +cascadant +cascadent +cascaderiez +cascadiez +case +caseifiaient +caseifiera +caseifierons +casematai +casematerai +casemateront +caseraient +caseriez +casernasse +casernement +casernerent +caserniez +casez +casilleuses +casinotieres +casquait +casquates +casquerait +casquetterie +casquons +cassames +cassasses +casse +casserai +casserions +cassette +cassier +cassissier +cassot +castagnais +castagnat +castagnerais +castagnettes +castant +castasses +castelets +casterai +casteront +castillanes +castor +castraient +castrasse +castrations +castrerai +castreront +castrum +casuiste +catabolismes +cataclysmes +catadromes +catalane +cataloguames +catalogue +catalogueras +cataloguez +catalysait +catalysates +catalyserait +catalyseurs +catameniale +cataplasmes +catapultais +catapultat +catapultez +catarrhales +catastase +catastrophez +catatonie +catchames +catche +catcherez +catchez +catechetique +catechisant +catechisiez +categorisa +categorisees +catelles +cathare +cathedraux +catheterisai +catheterisme +cathodique +catholicites +catilinaires +catinait +catinates +catinerait +catiniez +catirai +catiront +catisses +catites +caucasien +caudales +caudines +caulicoles +causal +causames +causassions +causees +causerez +causeur +caussenarde +caustiques +cauterisai +cauteriser +cautionnai +cautionner +cavage +cavalaient +cavalassions +cavalcadas +cavalcader +cavale +cavaleras +cavaleur +cavaliez +cavassions +cavee +caverent +cavernicoles +caviardages +caviardasses +caviardera +caviarderons +cavicorne +cavite +cayenne +ceans +cebuanos +ceda +cedas +cedees +cederez +cedex +cedraies +cedulaires +cegesimale +ceignent +ceignisses +ceindrait +ceinte +ceinturames +ceinture +ceintureras +ceinturions +celames +celat +celebrants +celebrations +celebrerait +celebrez +celent +celeri +celes +celibat +celinienne +cellas +celliers +cellulases +celluloid +celte +celtisme +cementa +cementasses +cementent +cementeriez +cementions +cendrais +cendrat +cendrerais +cendreuse +cendrons +cenobitismes +cense +censiere +censoriale +censurai +censurassiez +censurerai +censureront +centauree +centennale +centibars +centile +centon +centraient +centralisa +centralisee +centralismes +centrant +centrates +centrerai +centreront +centrifugeai +centrisme +centrosomes +centuplaient +centuples +centurions +cepes +ceraistes +ceramiste +ceratopsiens +cerceaux +cerclait +cerclates +cerclerait +cercliez +cerdagnole +cerebral +ceremoniaux +cereuse +cerisettes +cermets +cernas +cerneaux +cerneras +cernions +certaine +certifiait +certifier +certitudes +cerumens +cervaison +cervicale +cervides +cesariens +cesariser +cesars +cessait +cessassiez +cesser +cesserions +cession +cestodoses +ceteau +cetoine +cetonique +cevadille +ceylanaises +chabanous +chablais +chablat +chablerais +chablez +chabrol +chaconne +chaenichthys +chafouines +chagrinante +chagrinates +chagrinerait +chagriniez +chahutai +chahutassiez +chahuterai +chahuteront +chahuts +chainait +chainasses +chainera +chainerons +chaineurs +chainon +chais +chalandage +chalazions +chalcopyrite +chaldeen +challengeai +challengers +chalone +chaloupames +chaloupe +chalouperas +chaloupiers +chalutai +chalutassiez +chaluterais +chalutez +chamailla +chamaillera +chamailliez +chamaniste +chamarras +chamarrees +chamarrerez +chamarrure +chambardas +chambardees +chambarderas +chambardions +chamberiens +chamboulas +chamboulees +chambouleras +chamboulions +chambranlez +chambrera +chambrerons +chambrier +chameaux +chamitique +chamoisames +chamoise +chamoiseras +chamoisette +chamoisions +champagnes +champagnisez +champenoises +champignons +champleure +champlevasse +champlevent +champlures +chancel +chancelants +chanceler +chancellent +chanceux +chancirait +chancissais +chancissure +chancreux +chandelles +chanfreiner +changeable +changeants +changees +changerait +changeurs +channe +chansonna +chansonnera +chansonniez +chantai +chantasse +chantee +chanteraient +chanterons +chantiez +chantonna +chantonnent +chantoung +chantrerie +chaos +chaouis +chapardames +chaparde +chaparderas +chapardeuse +chapeauta +chapeautasse +chapeautent +chapee +chapelant +chapelee +chapelieres +chapelleront +chaperonnait +chaperonne +chaperonnons +chapitral +chapitrerai +chapitreront +chapo +chaponnant +chaponneau +chaponnerait +chaponneurs +chapskas +chaptalisent +charabia +charancon +charbonnage +charbonner +charbonneux +charcuta +charcutant +charcutee +charcuterent +charcutier +chardonna +chardonnent +chardonnons +chargeait +chargeates +chargeraient +charges +charia +chariotames +chariote +charioteras +chariotions +charite +charlotte +charmantes +charme +charmeras +charmeuse +charnure +charognes +charpentons +charretier +charrettes +charriames +charrie +charrieras +charrieuse +charriotai +charrioterai +charroiera +charroieront +charroyai +charroyeur +charruaient +charrues +charterisat +charterisiez +chartistes +chartrier +chassai +chassasse +chasseenne +chasserai +chasserions +chassies +chaste +chasubliere +chataignant +chataignee +chataignier +chataires +chatelains +chatiais +chatiat +chatierais +chatieront +chatoiements +chatoierions +chatonnames +chatonne +chatonnerez +chatons +chatouillee +chatouilleux +chatoyais +chatoyasses +chatoyions +chatrasse +chatrent +chatreriez +chatriez +chattais +chattat +chatterais +chatteront +chattienne +chaudeau +chaudin +chauffai +chauffards +chauffee +chaufferent +chauffes +chauffons +chaulais +chaulat +chaulerais +chauleuse +chaumages +chaumasse +chaument +chaumeriez +chaumiez +chaussaient +chaussassent +chausser +chausserions +chaussions +chauvait +chauvine +chauvirai +chauviront +chaux +chavirais +chavirat +chavirerai +chavireront +chayotte +chebs +cheddites +cheffe +cheikh +chelatai +chelatassiez +chelaterai +chelateront +chelems +chelleenne +cheminaient +cheminent +chemineriez +cheminote +chemisait +chemisates +chemiserait +chemises +chemisons +chenaies +chenalasse +chenalera +chenalerons +chenaux +chenevotte +chenils +cheque +cherames +cherassions +cherchas +cherchees +chercherez +cherchez +chereraient +cheres +cherifats +cherimoliers +cherirez +cherissait +cherites +cherrais +cherrat +cherreras +cherriez +chervis +chetivite +chevaient +chevalait +chevalates +chevaleriez +chevalieres +chevant +chevauchai +chevauchasse +chevauchons +chevelue +cheverai +cheveront +chevez +chevillant +chevillates +chevillerait +chevilles +chevillons +chevretant +chevreter +chevrettais +chevrettat +chevretteras +chevrettions +chevron +chevronnas +chevronnees +chevronnerez +chevrons +chevrotames +chevrotera +chevroterons +chevrotons +chiadais +chiadat +chiaderais +chiadeur +chiais +chialasse +chialera +chialerons +chialons +chias +chiassiez +chibrames +chibre +chibrerez +chic +chicanassiez +chicanerai +chicanerions +chicaniere +chiches +chicle +chicos +chicotasse +chicotent +chicoteriez +chicotons +chicottasse +chicottent +chicotteriez +chicouangues +chiendent +chier +chieries +chiez +chiffonnera +chiffonnons +chiffrait +chiffrates +chiffres +chiffrons +chignassent +chignerai +chigneront +chihuahuas +chikwangues +chiloms +chimio +chimiste +chinaient +chinassions +chindais +chindat +chinderas +chindions +chinerais +chineur +chinoisaient +chinoiserait +chinoises +chinure +chip +chipas +chipees +chiperez +chipeuses +chipolata +chipotames +chipote +chipoteras +chipoteur +chippewas +chiquasse +chiquement +chiqueras +chiqueuse +chirales +chirographes +chiromancien +chiropteres +chirurgien +chisinoviens +chiure +chleuhes +chlinguasse +chlinguera +chlinguerons +chloral +chlorassions +chlorees +chlorerais +chloreuse +chlorique +chloroforme +chlorometrie +chloroquines +chlorurais +chlorurat +chlorurerai +chlorureront +choane +chocolatee +chocolatines +chofars +choieriez +choirait +choisies +choisirez +choisisse +choix +choledoques +cholerines +cholines +chomable +chomas +chomedus +chomeras +chomeuse +chondrosteen +chopames +chope +choperas +chopin +chopinassent +chopinerai +chopineront +choppai +choppassiez +chopperais +choppes +choquait +choquassent +choquer +choquerions +chorales +chorees +choriale +choristes +choroidienne +chortens +chosifiant +chosifierait +chosifiiez +chouannai +chouannerais +chouanneront +chouchen +chouchoutait +chouchoute +chouchoutons +chougnait +chougnates +chougnerent +chougnons +chouinas +chouiner +chouinerions +choupette +chouraient +chourassions +chouravas +chouravees +chouraverez +choure +choureras +chourin +chouriner +chourins +choyaient +choyassions +choyiez +chrisme +christs +chromas +chromatides +chromatisait +chromatise +chromatisme +chromatopsie +chromerais +chromeur +chromique +chromisasse +chromisee +chromiserent +chromisons +chronicisat +chronicisiez +chroniquames +chronique +chroniqueurs +chronogramme +chronometras +chronometres +chrysomeles +chrysoprases +chthonienne +chtoniens +chuchotant +chuchotee +chuchoterait +chuchotes +chue +chuintantes +chuinte +chuinterais +chuintez +chutames +chute +chuteras +chutez +chyleuse +chymosines +ciao +ciblai +ciblassiez +ciblerai +cibleront +ciboule +cicatrice +cicatrisas +cicatrise +cicatriseras +cicatrisions +cichlide +ciclant +ciclee +ciclerent +ciclons +cidrerie +ciels +cigares +cigariers +cigues +ciliees +cillasse +cillement +cillerent +cillons +cimentaient +cimentasses +cimentent +cimenterie +cimentieres +cimetieres +cinabres +cineaste +cinemascope +cinemographe +cinephilie +cineroman +cingalaise +cinglais +cinglasses +cinglera +cinglerons +cinnamomes +cinsaut +cintrant +cintree +cintrerent +cintriez +cipayes +cirage +cirassent +circaete +circoncimes +circonciriez +circoncisent +circonflexes +circonspects +circonvenir +circonvinmes +circonvoisin +circuit +circulais +circularisee +circularites +circulateurs +circules +cireraient +cires +ciriers +cirques +cirripedes +cisaillai +cisailler +cisalpines +ciselait +ciselates +ciseleraient +ciseles +cisellement +cisjurane +cispadanes +cistacee +cisticolide +cita +citait +citates +citent +citerieur +citerons +citiez +citrates +citronnades +citronnasses +citronnent +citronneriez +citronnons +civaites +civieres +civilisais +civilisat +civilisee +civiliserent +civilisons +civismes +clabaudant +clabaudent +clabauderie +clabaudeuses +clabotai +clabotassiez +claboterai +claboteront +cladogramme +clairance +clairettes +claironnant +claironnat +claironnez +clairsemait +clairsemates +clairsemiez +clairvoyants +clamasse +clameca +clamecasses +clamecera +clamecerons +clamer +clamerions +clamp +clampas +clampees +clamperez +clampions +clampsas +clampsees +clampserez +clams +clamsassent +clamser +clamserions +clandes +clanique +clapi +clapirais +clapissaient +clapissons +clapotaient +clapotassent +clapoter +clapoterions +clapotis +clappas +clappements +clapperez +claps +claquante +claquates +claquemurat +claquemuriez +claqueras +claquetai +claquetons +claquetterez +claquions +clarifiais +clarifiasses +clarifient +clarifieriez +clarines +clartes +classaient +classasse +classement +classerent +classeuses +classifia +classifiiez +clathrate +claudicantes +claudiquant +claudiquent +clauses +claustrant +claustrerais +claustrez +clavaires +clavardais +clavardat +clavarderas +clavardions +clave +clavelee +clavera +claverons +clavetames +clavete +clavette +clavistes +clayonna +clayonnasse +clayonnent +clayonneriez +clays +clebards +clement +clemenvilla +clenchasse +clenchent +clencheriez +clenchons +cleptomanies +clergymans +clericatures +clerouquies +clichames +cliche +clicheras +clicheur +clics +clients +clignassent +clignements +clignerez +clignota +clignotas +clignoterez +clignotions +climatisant +climatiseurs +clinamen +cliniciennes +clippai +clippassiez +clipperai +clipperont +clipsaient +clipsassions +clipseraient +clipses +cliquais +cliquassiez +cliquerai +cliqueront +cliquetante +cliquetates +cliquetons +cliquions +clissant +clissee +clisserent +clissons +clitorisme +clivais +clivat +cliverais +clivez +cloaque +clocharde +clochardises +clochassiez +clocherai +clocheront +clochons +cloisonnera +cloitra +cloitrasses +cloitrera +cloitrerons +clonage +clonant +clone +cloneras +clonies +clopais +clopat +cloperais +clopet +clopinant +clopinent +clopineriez +clopions +cloppant +cloppent +clopperiez +cloquage +cloquassent +cloquer +cloquerions +cloraient +clos +closiez +cloturaient +clotures +clouaient +clouassions +clouera +clouerons +cloups +cloutait +cloutassions +clouteraient +clouterons +cloutons +clowns +clubbeur +clunisienne +clusiacees +cnidoblastes +coacervats +coachasse +coachent +coacheriez +coachons +coactive +coagulaient +coagulassent +coagulatrice +coagulerais +coagulez +coalescait +coalescates +coalescer +coalisaient +coalises +coaptations +coassais +coassat +coasserais +coassez +coassuraient +coassurasses +coassurera +coassurerons +coatis +cobaea +cobaye +cobier +cocagne +cocainomanes +cocardiers +coccidies +cochaient +cochassions +cochent +cocheres +cochette +cochleaires +cochonnaient +cochonnasses +cochonnera +cochonneriez +cochonnons +cocktails +cocolasse +cocolent +cocoleriez +coconisation +coconnant +coconnent +coconneriez +coconnons +cocoonais +cocoonat +cocoonerais +cocoonez +cocota +cocotasses +cocoteraie +cocoterons +cocottai +cocottassiez +cocotterais +cocottez +cocu +cocufiais +cocufiat +cocufierais +cocufiez +codai +codasse +codebitrice +codecidas +codecidees +codeciderez +codecision +coderas +codetenait +codetiendras +codetient +codetintes +codicille +codifiai +codifiassiez +codifie +codifieras +codifiions +codirigea +codirigera +codirigerons +codominantes +coechangiste +coeditasse +coeditent +coediteriez +coeditions +coelacanthe +coemption +coepouse +coercible +coeternelles +coexistait +coexistates +coexisterait +coexistiez +coffins +coffras +coffrees +coffrerez +coffrez +cofinancait +cofinancates +cofinances +cogerante +cogerates +cogererait +cogeriez +cogitais +cogitat +cogiterai +cogiteront +cognacs +cognasses +cognatiques +cognerai +cogneront +cohabitait +cohabiterai +cohabiteront +coherentes +coheritant +coheritent +coheriteriez +coheritiez +cohesives +cohobassent +cohobees +cohoberez +cohorte +coiffais +coiffasses +coiffera +coifferons +coiffons +coincait +coincates +coinceraient +coinces +coinchant +coinchee +coincherent +coinchons +coincidente +coinciderent +coincidons +coins +coitames +coite +coiterez +coitron +cokages +cokefiait +cokefiassiez +cokefierai +cokefieront +cokeurs +colature +colchique +colee +colereuse +colicitantes +colineaires +colis +colisas +colisees +coliserez +colistier +colitigantes +collaborant +collaborent +collage +collais +collapsais +collapsat +collapseras +collapsions +collassions +collateur +collations +collectait +collectates +collecterait +collecteurs +collectivisa +collectivise +collectons +collegiale +collegue +colleraient +collerons +colletaillee +colletait +colletates +colletiez +colleur +collige +colliger +colligerions +collimations +collocation +collons +colloquer +collusion +collyre +colmataient +colmates +colombage +colombiennes +colombite +colon +colonial +colonisa +colonisasse +colonises +colonnettes +coloquinte +colorant +colorat +colorectale +colorerais +colorez +colorias +coloriees +colorierez +colorimetres +colorisais +colorisat +coloriserai +coloriseront +coloscope +colosses +colportas +colportees +colporterez +colportez +coltan +coltinames +coltine +coltineras +coltinez +columbide +colvert +comanche +comatais +comatat +comateras +comateux +combatif +combattant +combattifs +combattit +combattrais +combattue +combinames +combinassiez +combinee +combinerent +combiniez +comblait +comblassions +comblera +comblerons +combrieres +combustions +comedons +cometiques +comiques +comitative +comitiaux +commandames +commandera +commanderiez +commandiez +commanditant +commanditees +commandons +commemorat +commemorees +commemorerez +commenca +commencas +commencees +commenceras +commencions +commentais +commentat +commenter +commerages +commerasses +commercames +commercerait +commercial +commerera +commererons +commettages +commettes +commettras +comminutif +commissaire +commissionne +commissuraux +commode +commotionne +commotions +commuas +commuees +commuerez +communal +communalisas +communaliser +communardes +communes +communiants +communicable +communier +communiquai +communiquez +communiste +commutait +commutates +commutes +compacite +compactais +compactat +compacterais +compacteur +compagnies +comparable +comparaitrai +comparassiez +comparatiste +comparent +compareriez +comparse +compassait +compassates +compasses +compatirait +compatissais +compatissiez +compendium +compenetrons +compensas +compensatif +compense +compenseras +compensions +competait +competates +competerai +competeront +competis +compilait +compilates +compilent +compileriez +compissa +compissasses +compissera +compisserons +complairai +complairont +complant +complanter +complementai +complementes +completais +completat +completerai +completeront +completons +complexant +complexat +complexerais +complexez +complexifiez +complication +complimente +complimentez +compliquames +complique +compliqueras +compliquions +complotas +complotees +comploterez +complotez +complussent +componees +comportais +comportat +comportent +comporteriez +composa +composantes +compose +composeras +composeuse +compositions +compostai +composterai +composteront +compotai +compotassiez +compoterai +compoteront +compoundage +compounder +compradore +comprendrait +comprenettes +compresses +compression +comprimais +comprimat +comprimerais +comprimez +comprisses +comptaient +comptassiez +compterai +compteront +comptions +compulsant +compulserait +compulsiez +compulsives +computeurs +comtale +comtoise +conasses +concassages +concassasses +concassera +concasserons +concatenai +concatener +concavite +concedasse +concedent +concederiez +concelebrai +concelebrera +concentrait +concentrates +concentrera +conceptacle +conceptrice +concernaient +concernes +concertait +concerter +concertino +concessibles +concevable +concevons +concevrons +conchiait +conchiates +conchierait +conchiiez +concierge +conciliantes +conciliateur +conciliees +concilierez +concis +concitoyens +concluante +concluons +concluriez +conclusives +concoctaient +concoctes +concombre +concordai +concordants +concordates +concorderas +concordions +concourent +concourrai +concours +concreta +concretasses +concreter +concretisees +concrets +concubines +condamnait +condamnates +condamnee +condamnerent +condamnons +condensant +condenserai +condenseront +condescendit +condisciple +condominiums +condrusien +conductible +conduira +conduirons +conduisez +condylien +confection +confederai +confederates +confedererai +conferais +conferat +confererez +conferve +confessasse +confessent +confesseriez +confettis +confiante +confiates +confieraient +confies +configurasse +configuree +configurons +confinant +confinee +confinerait +confiniez +confirais +confirmaient +confirmasse +confirmes +confisait +confiserie +confisons +confisquer +confisses +conflictuel +confluait +confluates +confluerai +conflueront +confondant +confondions +confondons +confondrions +conformaient +conformees +conformerent +conformisme +conforter +conforts +confreries +confrontas +confronte +confronteras +confrontions +confus +congeables +congedias +congediees +congedieras +congediions +congelant +congelateurs +congeles +congenitale +congestionna +congestionne +conglomerai +conglomere +conglomerons +conglutines +congratulat +congratules +congreait +congreates +congreerait +congressiste +congrues +conicines +conirostres +conjecturant +conjecturee +conjoignions +conjoignons +conjoncteurs +conjonctives +conjugalites +conjuguant +conjuguee +conjuguerent +conjuguons +conjuras +conjuration +conjurer +conjurerions +connaissent +connait +connaitrions +conneau +connectait +connectates +connecterait +connecteurs +connectique +connes +connivents +connotassent +connotative +connoterais +connotez +connusse +conopees +conqueriez +conquerrez +conquiere +conquisse +cons +consacrasse +consacrent +consacreriez +consanguine +consecutions +conseillais +conseillat +conseillons +consentais +consentes +consentirai +consentiront +conservais +conservat +conserverais +conserveront +considera +consideras +considerera +consignais +consignat +consigne +consigneras +consignions +consistant +consistat +consisteras +consistions +consoeurs +consolant +consolat +consolees +consolerez +consolidai +consolider +consolons +consommait +consommates +consommee +consommerent +consommons +consonai +consonerait +consoniez +consortiales +conspiraient +conspiration +conspirer +conspuaient +conspues +constant +constatames +constatation +constaterais +constatez +constellames +constellez +consternant +consternat +consternerai +constipait +constiper +constituer +constitution +construirez +construise +consulta +consultantes +consultatif +consulter +consultrice +consumant +consumee +consumerent +consumes +contactai +contacterai +contacteront +contagionna +contagionnat +contaminait +contamine +contamineras +contaminions +contates +contemplant +contemplee +contemplons +contenaient +conteneurisa +conteneurise +content +contenterez +contentiez +contenue +conterent +contestai +contestasse +contestera +contesterons +conteuses +contextuel +contiendrais +contiennes +continence +continentes +continrent +continua +continuasses +continuer +continuites +contondante +contorsions +contournasse +contournons +contractai +contractasse +contractent +contracturee +contraignant +contraindra +contrais +contrariais +contrariera +contrasta +contrastas +contrastees +contrasterez +contrastions +contre +contrebattit +contreboute +contrebraqua +contrebraque +contrebutant +contrebutees +contrebutons +contrecle +contrecolle +contrecoup +contredirais +contredis +contredises +contredit +contrefera +contreferont +contrefirent +contrefort +contrefoutes +contrefoutre +contremaitre +contremandas +contremandes +contrepet +contreplaqua +contreplaque +contreprojet +contrerent +contreseings +contresignez +contretypais +contrevenant +contreventa +contreventat +contrevient +contrevintes +contribuant +contribuent +contrions +contrister +contrites +controlaient +controlent +controleriez +controliez +controuvait +controuvates +controuviez +contumaces +contusionne +contusions +convaincra +convaincrons +convainquant +convectives +convenez +convention +convenues +convergeas +convergent +convergerait +convergiez +conversas +converserent +conversiez +converties +convertirez +convertissez +convexite +conviasse +convicts +conviendrez +convier +convierions +convinsse +convivial +convoient +convoierons +convoitait +convoitates +convoiterait +convoiteurs +convolai +convolassiez +convolerais +convolez +convoquas +convoquees +convoquerez +convoya +convoyasse +convoyer +convoyons +convulser +convulsionne +cooccupants +coolie +cooperantes +cooperateur +cooperatrice +coopereras +cooperions +cooptasse +cooptee +coopterent +cooptons +coordinences +coordonnants +coordonnes +coorganisee +copahu +coparent +copartageai +copartager +copermutait +copermutates +copermutiez +copiage +copiassent +copier +copierions +copiez +copilotames +copilote +copiloteras +copilotions +copinant +copinent +copinerie +copiniez +coplas +copossedas +copossedees +copossederez +copossesseur +copresida +copresident +copresideras +copresidions +coprocesseur +coproduira +coproduirons +coproduisez +coproduisit +coprolithes +coprophagies +coprosterols +copulait +copulates +copulatrices +copulerent +copulons +coquatres +coqueleuse +coqueluches +coqueret +coqueta +coquetasses +coquetez +coquettement +coquetteries +coquillaient +coquillasse +coquillent +coquilleriez +coquillieres +coquinet +coracoide +corallienne +coralline +coranisait +coranisates +coraniserait +coranisiez +corbillard +cordage +cordassent +cordee +cordelasse +cordeler +cordeliez +cordellerez +corderai +corderions +cordiale +cordierites +cordoba +cordonnai +cordonnerai +cordonniers +cordouans +coreennes +coregone +corfiotes +coricides +cormes +cornacee +cornalines +cornaquas +cornaquees +cornaquerez +cornard +cornat +corneillards +cornements +cornera +cornerons +corneur +cornichon +cornillons +corollaire +coronarien +coronavirus +corossol +corporation +corporeites +corpulence +corpuscules +correcteurs +correctrice +correlames +correlateur +correlatives +correlerais +correlez +correspondu +corrigea +corrigeasse +corrigeons +corrigeriez +corrigible +corroborant +corroborat +corroborees +corroborerez +corroda +corrodas +corrodees +corroderez +corroi +corroierie +corrompait +corrompis +corromprai +corrompront +corrosions +corroyames +corroye +corroyeuse +corruptible +cors +corsames +corse +corserais +corset +corsetassent +corseter +corseteries +corsetiers +corsophone +corticaux +cortinaire +corton +corvee +corydalis +cosaques +cosignant +cosignates +cosignes +cosmetiquait +cosmetique +cosmetiquons +cosmochimies +cosmologues +cossant +cossassions +cosserait +cossettes +cossues +costaud +costumai +costumassiez +costumerai +costumeront +cosy +cotangente +cotates +cotelees +coterait +cotes +cotices +cotieres +cotingas +cotiras +cotisaient +cotisassent +cotisees +cotiserez +cotissaient +cotissons +cotoierai +cotoies +cotonnait +cotonnates +cotonnerait +cotonnes +cotonnions +cotoyait +cotoyates +cotoyons +cottai +cottassiez +cotterai +cotteront +cotuteur +cotyledons +couacs +couchage +couchaillant +couchailler +couchant +couchat +coucherais +coucheront +couchiez +coucou +coudai +coudassiez +couderai +couderont +coudoient +coudoierons +coudoyait +coudoyates +coudoyons +coudres +couds +couffa +cougnou +couillonna +couillonner +couillues +couinasse +couinent +couineriez +coulabilite +coulante +coulates +couleraient +coules +coulissais +coulissasses +coulissement +coulisserent +coulissiez +coulombs +coumarines +coupage +coupaillant +coupaillee +coupaillons +coupassai +coupasserai +coupasseront +coupees +coupellees +coupellerez +coupement +couperent +couperoses +coupeuse +couplais +couplat +couplerai +coupleront +coupoir +coups +courageuses +couraillames +couraille +couraillerez +courailleux +courantes +couratant +couratent +courateriez +courbage +courbarine +courbattu +courbatturas +courbattures +courbaturai +courbaturez +courbent +courberiez +courbez +courcaillat +courcailliez +courette +courgettes +couronna +couronnasses +couronnent +couronneriez +couros +courreries +courroie +courroucant +courroucee +courroucons +coursas +coursees +courserez +coursieres +coursons +courtaudames +courtaude +courtauderas +courtaudions +courtiere +courtines +courtisat +courtiserais +courtisez +courtraisien +courus +cousais +couses +cousin +cousinames +cousine +cousineras +cousinions +cousissions +cousu +coutames +coutates +couteliers +couteras +couteuses +couts +couturaient +coutures +couvade +couvames +couve +couveraient +couverons +couveuses +couvrais +couvres +couvrira +couvrirons +couvrons +covalents +covenants +covoiturais +covoiturat +covoiturez +coxai +coxalgiques +coxasses +coxer +coxerions +coyau +crabieres +crabotames +crabote +craboteras +crabotions +crachant +crache +cracherais +cracheur +crachine +crachotai +crachotasse +crachotement +crachoterent +crachotons +cracovienne +cradoque +craignant +craignisse +craillai +craillassiez +craillerai +crailleront +craindrait +crainte +cramaient +cramassions +cramees +cramerez +cramions +crampaient +crampassions +cramperaient +crampes +cramponnais +cramponnent +cramponnons +cranas +cranees +cranerent +craneurs +craniez +cranioscopie +cranta +crantasse +crantent +cranteriez +crantiez +crapahutas +crapahutees +crapahuterez +crapaud +crapautaient +crapautes +crapotai +crapotassiez +crapoterais +crapoteuse +crapoussine +crapuleuses +craquant +craquat +craquelais +craquelat +craquelez +craquellerai +craquelles +craqueraient +craques +craqueterent +craquetterai +craquettes +crasha +crashasses +crashera +crasherons +crassa +crassasse +crassent +crasseriez +crassiers +craterelle +cratons +cravachant +cravachee +cravacherent +cravachons +cravatassent +cravater +cravaterions +crawl +crawlassent +crawler +crawlerions +crawlions +crayonnage +crayonner +crayonnions +creance +creasses +creatines +creatiques +crecelles +crechas +crecher +crecherions +credibilisa +credibilisat +credit +creditassent +crediter +crediterions +creditistes +credulite +creerai +creeront +cremaillere +cremasses +crematistes +cremera +cremeriez +cremiere +crenages +crenasses +crenela +crenelasse +creneler +crenelons +creneras +crenions +creolisai +creolisasse +creolisee +creoliserent +creolisme +creosol +creosotant +creosotee +creosoterent +creosotons +crepas +crepees +crepelure +creperas +crepez +crepiez +crepirai +crepiront +crepisses +crepitaient +crepitassent +crepitements +crepiterez +crepon +crepuscule +cresyls +cretait +cretates +creteraient +cretes +cretinisees +cretiniserez +cretinismes +cretonne +creusait +creusates +creuseraient +creuses +creusure +crevaisons +crevards +crevassasse +crevassent +crevasseriez +crevates +creverait +crevettes +crevotais +crevotat +crevoteras +crevotions +criaillaient +criaillerons +criaillons +criardes +crib +criblas +criblees +criblerez +criblez +cricoide +criera +crierons +crime +criminalises +criminel +crinieres +criqua +criquasses +criqueraient +criques +crisaient +crisassions +criserait +criseuses +crispait +crispassiez +crisper +crisperions +criss +crissassent +crisser +crisserions +crithmum +criticaillez +critiqua +critiquasse +critiquent +critiqueriez +critiquiez +croassas +croassements +croasserez +croate +crochais +crochat +crocherais +crochet +crochetames +crochete +crocheteras +crocheteuse +crocheuses +crochirai +crochiront +crochissiez +crochus +crocus +croire +croisai +croisassiez +croiser +croiserions +croisieres +croissait +croissent +croitrai +croitront +crollas +crollees +crollerez +cromalin +croqua +croquas +croquees +croquenots +croquerez +croqueur +cross +crossassent +crosser +crosserions +crossmans +crotchons +crottas +crottees +crotterez +crottions +croulant +croulat +croulerai +crouleront +croupale +croupieres +croupionnent +croupira +croupirons +croupisse +croupites +croutaient +croutassions +crouteraient +croutes +crown +croyant +cru +cruchons +crucifiai +crucifier +crucifixions +cruel +cruiser +crurales +crussions +crutes +cryogenes +cryometrique +cryotron +cryptait +cryptassions +crypteraient +cryptes +cryptomerias +ctenaires +cubai +cubas +cube +cuberais +cuberont +cubis +cubitaux +cuchaules +cucurbitacee +cucuteries +cueillent +cueillerons +cueilli +cueillissent +cuestas +cuillere +cuiraient +cuirassant +cuirassee +cuirasserait +cuirassier +cuirions +cuisantes +cuisina +cuisinasses +cuisinera +cuisinerons +cuisiniez +cuisisses +cuissards +cuistance +cuita +cuitasses +cuitera +cuiterons +cuivrages +cuivrasses +cuivrera +cuivreriez +cuivrions +culames +culat +culbutant +culbutee +culbuterait +culbuteurs +culent +culeriez +culiere +culminais +culminasses +culminera +culminerons +culotta +culottasse +culottent +culotteriez +culottiez +culpabilise +culpabilite +cultismes +cultivar +cultivateur +cultiverai +cultiveront +cultural +culturelle +culturiste +cumulable +cumulardes +cumulatif +cumulera +cumulerons +cunicoles +cupides +cupriferes +curage +curare +curarisant +curarisat +curariserai +curariseront +curassiez +curative +curees +curerez +curetaient +curetassions +curetiez +cureur +curiaux +curieux +curleur +curriculums +cursus +cuscutacees +cussonnes +customisant +customisiez +cuticulaire +cutter +cuvait +cuvates +cuvelaient +cuvelassions +cuveliez +cuvellerez +cuverai +cuverions +cuvions +cyanees +cyanosaient +cyanoses +cyanurait +cyanurates +cyanures +cybercameras +cyberguerres +cyberspatial +cyclables +cyclaniques +cyclines +cyclisant +cyclisations +cycliserait +cyclisiez +cycloalcene +cyclopeen +cyclopiennes +cycloramas +cylindrages +cylindrasses +cylindrent +cylindreriez +cylindriez +cymaises +cymbalistes +cymriques +cyniques +cynologies +cyons +cypriere +cyrilliques +cysticerques +cystostomies +cytises +cytologiques +cytopenie +cytotropisme +dabas +dacites +dacryadenite +dactylo +dactylos +dagua +daguasses +daguera +daguerons +daguet +dahirs +daigna +daignasses +daigneraient +daignes +daille +daines +dakins +dallaient +dallassions +dalleraient +dalles +dalmatienne +daltoniennes +damaient +damascene +damasquinant +damasquinees +damasquiniez +damassames +damasse +damasseras +damassez +dame +dameras +dameuse +damnai +damnassiez +damner +damnerions +damoiselle +dances +dandinames +dandinera +dandinerons +dandy +dangerosites +danoises +dansant +dansat +danserais +danseur +dansotaient +dansoterait +dansotiez +dansottas +dansotter +dantoniste +daphnie +darbyste +dardames +darde +darderas +dardillon +daris +darons +dartre +darwiniens +datable +datames +datation +daterai +daterions +datif +dattiers +daubas +daubees +dauberez +daubez +dauphinelles +davantage +dayaks +dealais +dealat +dealerais +deales +deambula +deambulasses +deambulerent +deambulons +debachant +debachee +debacherent +debachons +debaclassent +debaclements +debaclerez +debadge +debadger +debadgerions +debagoulai +debagoulerai +debaguait +debaguates +debaguerait +debaguiez +debaillonnas +debaillonnes +deballais +deballastage +deballerai +deballeront +deballonnai +deballonnez +debalourdee +debanalisa +debanaliser +debandai +debandassiez +debanderai +debanderont +debaptisait +debaptisates +debaptisiez +debarcadere +debardant +debardee +debarderent +debardeuses +debaroulames +debaroule +debarouleras +debaroulions +debarquasse +debarquement +debarquerent +debarquons +debarrassai +debarrassez +debarrer +debarrerions +debarulaient +debarules +debatait +debatates +debaterait +debatez +debatiraient +debatis +debatissions +debatte +debattiez +debattit +debattrez +debauchage +debaucher +debecquetat +debecquetiez +debectas +debectees +debecterez +debenture +debenzolant +debenzolee +debenzolons +debequeter +debile +debilitames +debilitera +debiliterons +debillardais +debinant +debinee +debinerent +debineuses +debita +debitant +debitat +debiterais +debiteur +debitrice +deblais +deblaterer +deblayages +deblayasses +deblayera +deblayerons +debloquai +debloquasse +debloquent +debloqueriez +debobinai +debobinerai +debobineront +deboguaient +debogues +deboisage +deboisassent +deboisements +deboiserez +deboita +deboitasse +deboitement +deboiterent +deboitons +debondassent +debonder +debonderions +debonnaires +debordant +debordat +deborderai +deborderont +debossai +debossassiez +debosselai +debosseleur +debosselons +debosserez +debossez +debottant +debottee +debotterent +debottons +debouchas +debouchees +deboucheras +debouchez +debouclait +debouclates +debouclerait +deboucliez +debouillez +debouillis +debouillons +deboulassent +debouler +deboulerions +deboulonnai +deboulonnera +debouquais +debouquat +debouquerais +debouquez +debourbait +debourbates +debourberait +debourbeurs +debourrage +debourrerez +debourrez +deboursames +debourse +debourserais +deboursez +deboussolee +debout +deboutassent +deboutements +debouterez +deboutonnage +deboutonnera +debraguettee +debraient +debraierons +debraillas +debraillees +debraillerez +debrancha +debranchent +debrasage +debrasassent +debraser +debraserions +debrayables +debrayasse +debrayent +debrayeriez +debreakai +debreakerai +debreakeront +debridaient +debridera +debriderons +debriefais +debriefat +debrieferais +debriefez +debrochai +debrocherai +debrocheront +debronzait +debronzates +debronzerait +debronziez +debrouiller +debroussai +debroussais +debroussat +debrousserai +debrutir +debrutirions +debrutisses +debuchaient +debuchers +debudgetisee +debuggai +debuggassiez +debuggerai +debuggeront +debusquage +debusquerez +debut +debutanisais +debutassiez +debuterai +debuteront +decabriste +decachetait +decachetates +decachetons +decadenassee +decadences +decadra +decadrasse +decadrent +decadreriez +decaedres +decafeiner +decagonales +decaissait +decaissates +decaisses +decalais +decalaminee +decalant +decalcifiai +decalcifient +decale +decaleras +decalions +decalottait +decalottates +decalottiez +decalquames +decalque +decalqueras +decalquions +decampa +decampasses +decampera +decamperons +decanales +decanillant +decanillent +decanoiques +decantant +decantations +decanterait +decanteurs +decapai +decapasse +decapela +decapelasses +decapeles +decapements +decaperez +decapez +decapitat +decapiterai +decapiteront +decapons +decapotas +decapotees +decapoterez +decapsula +decapsulasse +decapsulee +decapsuliez +decarbonate +decarburante +decarburates +decarbures +decarcassee +decarrelait +decarrelates +decarrelle +decasyllabes +decatie +decatirent +decatissais +decatissez +decausait +decausates +decauserait +decausiez +decavasse +decavent +decaveriez +deccas +decedassent +deceder +decederions +decelables +decelasses +decelent +decelerasse +decelerent +decelereriez +deceles +decemvir +decennaire +decentes +decentrat +decentrer +deceptifs +decerclames +decercle +decercleras +decerclions +decerebrasse +decerebree +decerebrons +decernassent +decerner +decernerions +decervelages +decerveles +decevais +decevons +decevrons +dechainasse +dechainement +dechainerent +dechainons +dechantasse +dechantera +dechanterons +dechargeas +dechargement +dechargerais +dechargeur +decharnames +decharne +decharnerais +decharnez +dechaumait +dechaumates +dechaumerait +dechaumeuses +dechaussais +dechaussat +dechausserai +dechaux +dechevetrai +dechevetrez +dechiffonner +dechiffrages +dechiffrent +dechiffriez +dechiquetee +dechiquetiez +dechirai +dechirasse +dechirement +dechirerent +dechirons +dechloruras +dechlorurent +dechoie +dechoirez +dechoquai +dechoquerai +dechoqueront +dechussiez +decidable +decidassent +decident +decideriez +decidiez +decidus +decilitres +decillassent +deciller +decillerions +decimaient +decimalisait +decimalisiez +decimasses +decime +decimeras +decimetrique +decintrai +decintrer +decisifs +decisivement +declamames +declamateur +declamees +declamerez +declara +declarantes +declaratif +declarees +declarerez +declassa +declassasses +declassent +declasseriez +declassifiee +declassons +declenchai +declenchasse +declinais +declinasse +declines +declinquais +declinquat +declinquez +decliquetais +decliquetons +decloisonnee +declora +declorons +declosons +declouant +declouee +declouerent +declouons +decochant +decochee +decocherait +decochiez +decodai +decodassiez +decoderai +decoderont +decoffra +decoffrasse +decoffrent +decoffreriez +decohabitai +decohabitez +decoiffaient +decoifferez +decoinca +decoincasse +decoincement +decoincerent +decoincons +decolerait +decolerates +decolererent +decolerons +decollames +decollation +decolles +decolletas +decolletees +decolletions +decollons +decolonisent +decoloraient +decolorees +decolorerez +decombres +decommandera +decommettait +decommettrai +decommunisas +decommuniser +decompactat +decompactiez +decompensas +decompensee +decomplexa +decomplexes +decomposais +decomposat +decomposeur +decompresse +decomprimas +decompriment +decomptaient +decomptes +deconcertera +deconfire +deconfisait +deconfissent +decongela +decongelent +decongestifs +deconnaient +deconnassent +deconnectais +deconnerent +deconneuses +deconseillee +deconsiderai +deconsigner +deconstipais +decora +decorasses +decorations +decordames +decorde +decorderas +decordions +decorerais +decorez +decornas +decornees +decornerez +decorons +decorrelees +decorrelerez +decors +decortiquee +decotai +decotassiez +decoterai +decoteront +decouchait +decouchates +decoucherent +decouchons +decoudriez +decouennames +decouenne +decouenneras +decouennions +decoulasse +decoulera +decoulerons +decoupai +decoupassiez +decouperai +decouperont +decouplage +decoupler +decoupons +decourageant +decourages +decouronnant +decouronnees +decouronnons +decousiez +decousit +decouverte +decouvrent +decouvrir +decouvrites +decramponnez +decrassait +decrassates +decrasses +decreditant +decreditee +decreditons +decrementer +decrepages +decrepasses +decrepera +decreperons +decrepir +decrepirions +decrepisse +decrepitera +decrepons +decretales +decretates +decreterait +decretiez +decreusait +decreusates +decreuses +decriait +decriates +decrierait +decriiez +decrira +decrirons +decrispas +decrispe +decrisperas +decrispions +decrive +decrivissent +decrochai +decrocher +decrochions +decroisas +decroisees +decroiseras +decroisions +decroissants +decroit +decroitrions +decrottait +decrottates +decrotterait +decrotteurs +decrottons +decruant +decruee +decruerent +decrumes +decrusait +decrusates +decruserait +decrusiez +decrypta +decryptasse +decryptement +decrypterent +decryptons +decuiras +decuisait +decuisis +decuite +decuivrames +decuivre +decuivreras +decuivrions +deculassasse +deculassent +deculottage +deculotter +decupla +decuplasses +decuplent +decupleriez +decurie +decuva +decuvant +decuvee +decuverent +decuvons +dedaignames +dedaigne +dedaigneras +dedaigneuses +dedaleenne +dediames +dedicaca +dedicacasses +dedicacera +dedicacerons +dedicatoires +dedierait +dedions +dediriez +dedisent +dedissions +dedommageait +dedommagee +dedommagez +dedorames +dedore +dedoreras +dedorions +dedouanait +dedouanates +dedouanes +dedoublais +dedoublat +dedoublerai +dedoubleront +dedramatisee +deduirai +deduiront +deduisiez +deduisit +defaillais +defailles +defaillirais +defaillisse +defaisaient +defaite +defalquai +defalquerai +defalqueront +defasses +defatiguais +defatiguat +defatiguez +defaufilant +defaufilee +defaufilons +defausser +defaveur +defavorisee +defecation +defectueuse +defendais +defendez +defendre +defendus +defenestrent +defenses +defequa +defequasses +defequera +defequerons +deferais +deferat +deferente +defererent +deferla +deferlantes +deferle +deferlerais +deferlez +deferraient +deferrera +deferrerons +defeuillames +defeuille +defeuilleras +defeuillions +defeutrant +defeutree +defeutrerent +defeutrons +defiant +defiat +defibrant +defibree +defibrerent +defibreuses +deficelai +deficelez +deficelleras +deficient +defiees +defierez +defigeaient +defigeraient +defiges +defigurant +defigures +defilai +defilassiez +defiler +defilerions +defilions +definiraient +definis +definisses +definitifs +definitoires +defiscalisee +defissent +deflagrait +deflagrer +deflataient +deflates +deflechies +deflechirez +deflechisse +deflecteurs +defleurira +defleurirons +defleurissez +defloqua +defloquasses +defloquera +defloquerons +deflorais +deflorassiez +deflorer +deflorerions +defluviation +defoliantes +defoliation +defolierais +defoliez +defoncait +defoncates +defonces +deforcaient +deforces +deforestais +deforestat +deforesterai +deformaient +deformassent +deformatrice +deformerais +deformez +defoulant +defoulat +defoulerai +defouleront +defouraille +defourna +defournasse +defournement +defournerent +defourneuses +defraichies +defraichirez +defraiements +defraierions +defranchirai +defrayant +defrayee +defrayerent +defrayons +defrichames +defriche +defricherais +defricheur +defripaient +defripes +defrisais +defrisat +defriserai +defriseront +defroisses +defroncames +defronce +defronceras +defroncions +defroquasse +defroquent +defroqueriez +defruitai +defruiterai +defruiteront +defuntais +defuntat +defunteras +defuntions +defusionnant +defusionnees +degage +degageassent +degagent +degagerez +degainai +degainassiez +degainerai +degaineront +degalonnait +degalonnates +degalonniez +degammas +degammer +degammerions +degantaient +degantes +degarnira +degarnirons +degarnissent +degasolina +degasoliner +degauchie +degauchirent +degazais +degazat +degazerais +degazez +degazolinee +degazonna +degazonnasse +degazonnons +degelas +degelees +degelerez +degels +degenassent +degener +degenerative +degenererais +degeneront +degermait +degermates +degermerait +degermiez +degingandas +degingandent +degivrages +degivrasses +degivrera +degivrerons +deglacage +deglacassent +deglacements +deglacerez +deglacions +deglinguant +deglinguee +deglinguons +degluasse +degluent +deglueriez +deglutie +deglutirait +deglutissais +deglutit +degobillait +degobillates +degobilliez +degoisas +degoisees +degoiseras +degoisions +degommant +degommee +degommerent +degommons +degondassent +degonder +degonderions +degonflables +degonflarde +degonflates +degonfles +degorgeaient +degorgeons +degorgeriez +degotaient +degotassions +degoteraient +degotes +degottames +degotte +degotteras +degottions +degoudronnez +degoulinait +degoulinerai +degoupillait +degoupille +degoupillons +degourdis +degourdisses +degoutaient +degoutasse +degoutee +degouterent +degoutons +degouttantes +degoutte +degoutterez +degrada +degradas +degradation +degraderais +degradez +degrafant +degrafee +degraferent +degrafeuses +degraffitant +degraffitees +degrafions +degraissant +degraissat +degraisseur +degravoyames +degravoye +degre +degreassent +degreements +degreerez +degres +degrevai +degrevassiez +degrever +degreverions +degriffaient +degriffes +degringolai +degringolez +degrippant +degrippe +degripperas +degrippions +degrisasse +degrisement +degriserent +degrisons +degrosser +degrossions +degrossiriez +degrouillant +degrouillees +degroupage +degrouperez +degue +deguerpira +deguerpirons +deguerpisses +degueulai +degueulassee +degueulera +degueulerons +deguillai +deguillerai +deguilleront +deguisait +deguisates +deguises +degurgitames +degurgitez +degustant +degustateurs +degustera +degusterons +dehalai +dehalassiez +dehalerai +dehaleront +dehanchait +dehanchates +dehanches +deharnachee +dehiscence +dehottais +dehottat +dehotteras +dehottions +dehouillasse +dehouillent +dehoussable +dehousser +deictique +deifiasse +deifiee +deifierent +deifions +dejantaient +dejantes +dejaugeaient +dejauges +dejaunirai +dejauniront +dejaunissiez +dejetais +dejetat +dejetions +dejetterez +dejeunait +dejeunates +dejeunerent +dejeunions +dejouasse +dejouent +dejoueriez +dejuchai +dejuchassiez +dejucherai +dejucheront +dejugeais +dejugeat +dejugerais +dejugez +delabrais +delabrat +delabrerai +delabreront +delaca +delacasses +delacera +delacerons +delaiera +delaieront +delainant +delainee +delainerent +delainons +delaissasse +delaissement +delaisserent +delaissons +delaitas +delaitees +delaiteras +delaitez +delardait +delardates +delardes +delassames +delassera +delasserons +delations +delattas +delattees +delatterez +delava +delavasse +delavent +delaveriez +delayage +delayassent +delayer +delayerions +deleatur +deleaturer +delecta +delectasse +delectee +delecterent +delectons +delegations +delegitimas +delegitimee +delegua +deleguasses +deleguera +deleguerons +delemontains +delestas +delestees +delesterez +deletere +deliant +deliassait +deliassates +deliasserait +deliasseuses +deliberais +deliberasses +delibererai +delibereront +delicatesse +delicieux +deliees +delierai +delieront +delignames +deligne +deligneras +delignez +delignifient +delignure +delimitant +delimiterait +delimiteurs +delineament +delineateur +delineerai +delineeront +delinquantes +delirai +delirasse +delirera +delirerons +delissage +delissassent +delisser +delisserions +delistaient +delistes +delitaient +delitassions +delitent +deliteriez +delitescents +delivrait +delivres +delocalisai +delocalisera +delogeais +delogeat +delogerai +delogeront +deloguames +delogue +delogueras +deloguions +deloquasse +deloquent +deloqueriez +delots +delovassent +delover +deloverions +deloyalement +deltacisme +deltoides +delurais +delurat +delurerais +delurez +delustrait +delustrates +delustrerait +delustriez +delutames +delute +deluteras +delutions +demagnetises +demagogue +demaigris +demaillais +demaillat +demaillerais +demaillez +demaillotant +demaillotees +demancha +demanchasses +demanchent +demancheriez +demandai +demandassiez +demanderai +demanderions +demandions +demangeames +demangee +demangeras +demangions +demantelerez +demantibula +demantibulat +demaquillait +demaquillez +demarcative +demarchant +demarchee +demarcherent +demarcheuses +demariais +demariat +demarierais +demariez +demarquait +demarquates +demarquerait +demarqueurs +demarrai +demarrassiez +demarrerai +demarreront +demasclages +demasclasses +demasclera +demasclerons +demasquais +demasquat +demasquerais +demasquez +demastiquat +demastiquiez +dematames +demate +demateras +demateriez +dematinais +dematinat +dematinerais +dematinez +demazoutes +demelant +demelat +demelerai +demeleront +demeloirs +demembrant +demembree +demembrerait +demembriez +demenageames +demenagee +demenagerais +demenageur +demenait +demenates +demeneraient +demenes +dementant +dementiels +dementirait +dementissent +demerdaient +demerdasse +demerdent +demerderiez +demerdiez +demeritas +demeriter +demersales +demettaient +demettons +demettrions +demeublas +demeublees +demeublerez +demeura +demeurasses +demeurera +demeurerons +demie +demieller +deminait +deminates +deminerait +deminerons +demis +demissionnat +demixtions +demobilisant +demobilisez +democratisa +democratisat +demodas +demodees +demoderez +demodons +demodule +demoduleras +demodulions +demoiselle +demolirais +demolissage +demolisseurs +demolitions +demonetisant +demonetise +demonetisons +demonisas +demonisees +demoniserez +demonismes +demontage +demontants +demontee +demonterent +demonteuses +demontrai +demontrerai +demontreront +demoralisait +demoralisees +demordaient +demordions +demordons +demordrions +demotivaient +demotivees +demotiverez +demoucheta +demouchetez +demoulaient +demoules +demoustiquai +demultipliee +demunie +demunirent +demunissant +demusela +demuselasses +demuseles +demutisai +demutiser +demystifie +demystifions +demythifier +denantimes +denantiriez +denantissent +denasalisa +denasaliser +denattait +denattates +denatterait +denattiez +denaturasses +denaturent +denatureriez +denazifiai +denazifier +dendritique +dendrolague +dendrometre +denebulait +denebulates +denebulera +denebulerons +denebulisait +denebulisiez +deneigeai +deneiger +deneigerions +denervai +denervassiez +denerver +denerverions +deni +deniaisames +deniaise +deniaiserais +deniaisez +deniasses +denichais +denichat +denicherais +denicheur +denierai +denieront +denigrant +denigrat +denigrerai +denigreront +deniiez +denitrait +denitres +denivelant +denivelee +denivelle +denivellerez +denoierai +denoies +denombras +denombrees +denombreras +denombrions +denomma +denommasses +denommera +denommerons +denoncais +denoncat +denoncerais +denoncez +denota +denotasses +denotatives +denoterait +denotiez +denouas +denouees +denoueras +denouions +denoyant +denoyautage +denoyauter +denoye +denree +densifiames +densifierais +densifiez +densite +dentaire +dentasse +dentees +dentelas +dentelees +dentelions +dentellerie +dentelliers +denterait +denticule +dentinaires +dentition +denuait +denuates +denudames +denudation +denuderais +denudez +denuer +denuerions +denutries +deontiques +depacsaient +depacses +depaillais +depaillat +depaillerais +depaillez +depalissait +depalissates +depalissiez +depannames +depanne +depanneras +depanneuse +depaquetiez +deparaient +deparasitat +deparasitiez +depare +depareillas +depareillent +deparera +deparerons +depariant +depariee +deparierent +deparions +deparlassent +deparlerai +deparleront +departagea +departagerai +departement +departie +departirait +departissais +departit +depassaient +depassassiez +depasser +depasserions +depatouilla +depatouillat +depatriasse +depatrient +depatrieriez +depavage +depavassent +depaver +depaverions +depaysaient +depaysassent +depaysements +depayserez +depeca +depecasse +depecement +depecerent +depeceuses +depechasse +depechent +depecheriez +depecions +depeignasse +depeignent +depeigneriez +depeignis +depeindrai +depeindront +depenalisa +depenaliser +dependaient +depende +dependions +dependons +dependrions +depensai +depensassiez +depenserai +depenseront +deperdition +deperiras +deperissait +deperissez +deperlantes +depetrassent +depetrer +depetrerions +depeuplaient +depeuplera +depeuplerons +dephasai +dephasassiez +dephaserai +dephaseront +dephosphorez +depiautait +depiautates +depiauterait +depiautiez +depigmentais +depigmentez +depilait +depilates +depilent +depileriez +depiquage +depiquassent +depiquer +depiquerions +depistables +depistasse +depistent +depisteriez +depistons +depitasse +depitent +depiteriez +deplaca +deplacasses +deplacent +deplaceriez +deplafonnai +deplafonnera +deplairait +deplaisais +deplaisez +deplanais +deplanat +deplanerais +deplanez +deplantais +deplantat +deplanterai +deplanteront +deplantinant +deplantinees +deplantoir +deplatrames +deplatre +deplatreras +deplatrions +depliait +depliassiez +deplier +deplierions +deplissages +deplissasses +deplissera +deplisserons +deploient +deploierons +deplombames +deplombe +deplomberas +deplombions +deplorames +deploration +deplorerais +deplorez +deployait +deployates +deployons +deplumasse +deplument +deplumeriez +deplus +depoetises +depointais +depointat +depointerais +depointez +depolarisent +depolies +depolirez +depolissait +depolissons +depolitisas +depolitisee +depollua +depolluas +depolluees +depolluerez +depolluions +deponentes +deportames +deportat +deporter +deporterions +deposai +deposasse +deposent +deposeriez +deposition +depossedames +depossede +depossederas +depossedions +depotais +depotat +depoterai +depoteront +depoudrai +depoudrerai +depoudreront +depouillera +depourvus +depoussierer +depravaient +depravassent +depravatrice +depraverais +depravez +deprecia +depreciasses +depreciera +deprecierons +depredations +deprends +depressif +deprimants +deprimee +deprimerent +deprimons +deprisas +deprisees +depriserez +deprisse +deprogramme +deprotegeat +deprotegez +depucelames +depucele +depucellent +depulpais +depulpat +depulperais +depulpez +depurant +depuratifs +depurera +depurerons +deputais +deputat +deputerai +deputeront +dequalifiait +dequillasse +dequillent +dequilleriez +deracina +deracinasse +deracinement +deracinerent +deracinons +deradassent +deraderai +deraderont +derageais +derageat +derageras +deragions +deraidirait +deraidissais +deraidit +deraierez +deraillait +deraillates +deraillerait +derailleurs +deraisonnat +deraisonniez +deramant +deramee +deramerent +deramons +derangeantes +derangee +derangerais +derangez +derapames +derape +deraperez +derasa +derasasses +derasent +deraseriez +deratai +deratassiez +deraterai +derateront +deratisames +deratisation +deratiserais +deratiseur +derayage +derayassent +derayer +derayerions +derayure +derealisai +derealisasse +derealisee +derealisons +dereglai +dereglassiez +deregleras +dereglions +deregulasse +deregulee +deregulerent +deregulons +deridages +deridasses +deridera +deriderons +derivait +derivassiez +derive +deriveras +derivetaient +derivetiez +derivetteras +deriviez +dermatos +dermiques +dernier +derobai +derobassiez +derober +deroberions +derobions +derochant +derochee +derocherait +derochiez +derodames +derode +deroderas +derodions +derogeais +derogeassiez +derogerais +derogez +deroquas +deroquees +deroquerez +derougi +derougiras +derougissait +derougites +derouillas +derouillees +derouillerez +deroula +deroulantes +deroule +deroulerais +derouleur +deroutages +deroutas +deroutees +derouteras +deroutions +derupitai +derupiterais +derupitez +desabonnai +desabonner +desabusaient +desabusera +desabuserons +desaccentue +desacclimata +desacclimate +desaccordee +desaccords +desaccouplez +desacidifiai +desacierer +desactivees +desactiverez +desadapta +desadaptent +desaerage +desaerassent +desaerees +desaererez +desaffecta +desaffecter +desaffiliais +desaffiliez +desagrafant +desagrafee +desagrafons +desagregeais +desagregiez +desaimantant +desaimante +desaimantons +desajusta +desajustent +desalienai +desaliener +desalignera +desalinisais +desalinisez +desalpant +desalpent +desalperiez +desalterai +desalterasse +desalterent +desamai +desamassiez +desamees +desamerez +desamiantai +desamiantez +desaminai +desaminees +desaminerez +desamions +desamorcant +desamorcee +desamorcons +desangoissas +desangoisses +desannexames +desannexe +desannexeras +desannexion +desannoncas +desannoncent +desapaient +desapassions +desaperaient +desapes +desappariee +desappointa +desappointat +desapprenez +desapprise +desapprouver +desarconnait +desarconnez +desaretant +desaretee +desareterent +desaretons +desargentera +desarmait +desarmassiez +desarmer +desarmerions +desarrimages +desarrimera +desarticulai +desassembler +desassimilee +desassortie +desastres +desatomisat +desatomises +desavantager +desavoua +desavouasses +desavouera +desavouerons +desaxais +desaxat +desaxerais +desaxez +descellant +descellee +descellerait +descelliez +descendante +descendeuse +descendisses +descendrait +descendue +descolarisai +descriptive +desechouages +desechouera +desemballas +desemballent +desembobine +desembourba +desembourbat +desembuames +desembue +desembueras +desembuions +desemparasse +desemparent +desempesai +desempeserai +desemplir +desemplisses +desencadrant +desencadrees +desencadrons +desenchantas +desenchanter +desenclavait +desenclave +desenclaviez +desencollee +desencombra +desencombrat +desencrames +desencrassas +desencrasses +desencrent +desencreriez +desendettai +desendettera +desenervait +desenervates +desenerviez +desenfilas +desenfilees +desenfilerez +desenfla +desenflasse +desenflent +desenfleriez +desenflures +desenfumames +desenfume +desenfumeras +desenfumions +desengageas +desenglua +desengluera +desengorgee +desengorgiez +desengrenee +desenivra +desenivrera +desenlacais +desenlacat +desenlacez +desenlaidis +desenlaidit +desennuierez +desennuyait +desennuyates +desennuyons +desenrayames +desenraye +desenrayeras +desenrayions +desensimes +desentoiles +desentraver +desenvasais +desenvasat +desenvasez +desenvenime +desenvergua +desenverguat +desepaissie +desequipames +desequipe +desequiperas +desequipions +desertas +desertees +deserterez +desertifia +desertifiera +desesperait +desesperasse +desespererez +desespoir +desetamant +desetamee +desetamerent +desetamons +desetatisent +desexcitera +desexualise +deshabilla +deshabiller +deshabituais +desherbant +desherbat +desherberais +desherbez +desheritait +desheritates +desherites +deshonneur +deshonore +deshonoreras +deshonorions +deshuilant +deshuilee +deshuilerent +deshuiliez +deshumanisee +deshydratiez +desideratums +designasse +designations +designers +designs +desilassent +desiler +desilerions +desincarnas +desincarnee +desincorpora +desincorpore +desincruste +desindexa +desindexent +desinences +desinfectat +desinformat +desinforment +desinhiber +desinscrite +desinscrivez +desinserames +desinsere +desinsereras +desinserions +desintegrais +desintegrez +desinvestira +desinvitas +desinvitees +desinviterez +desinvolte +desirais +desirasses +desirera +desirerons +desirs +desistassent +desistements +desisterez +desk +desmotropie +desobeirais +desobeissent +desobligeai +desobligees +desobstruent +desoccupees +desocialises +desodorisai +desodorisees +desoeuvree +desolames +desolassions +desolera +desolerons +desolons +desoperculez +desopilant +desopilat +desopilerais +desopilez +desorbitant +desorbitiez +desordonnas +desordonnent +desorganisa +desorganisat +desorganisez +desorientant +desoriente +desorientons +desossait +desossates +desosses +desoxydames +desoxydera +desoxyderons +desoxygenais +desoxygenez +despote +desquamais +desquamasses +desquament +desquameriez +desquels +dessablas +dessablees +dessableras +dessablions +dessaisirais +dessalaisons +dessalat +dessalerai +dessaleront +dessalures +dessangler +dessaoules +dessapames +dessape +dessaperas +dessapions +dessechantes +desseche +dessecherais +dessechez +dessellait +dessellates +dessellerait +desselliez +desserrames +desserre +desserrerais +desserrez +desserties +dessertirez +dessertites +desservez +desservis +dessevages +dessillai +dessillerai +dessilleront +dessinais +dessinat +dessiner +dessinerions +dessolai +dessolassiez +dessoler +dessolerions +dessouchages +dessouchera +dessoudais +dessoudat +dessouderais +dessoudez +dessoulait +dessoulates +dessoulerait +dessouliez +dessuintait +dessuintates +dessuintiez +destalinisez +destinames +destiner +destinerions +destituable +destituer +destocka +destockasse +destockent +destockeriez +destockons +destressames +destresses +destructeur +destructure +desuete +desulfitais +desulfitat +desulfitez +desulfurant +desulfuriez +desunira +desunirons +desunissez +detachages +detachas +detachees +detacheras +detacheuse +detaillais +detaillasses +detaillera +detaillerons +detalaient +detalassions +detalerait +detaliez +detalonnant +detalonnee +detalonnons +detapissasse +detapissent +detartrage +detartrants +detartree +detartrerent +detartriez +detassas +detassees +detasserez +detatoua +detatouasse +detatouent +detatoueriez +detaxai +detaxassiez +detaxer +detaxerions +detectables +detectasses +detectera +detecterons +detectives +deteignes +deteindras +deteintes +detelant +detelee +detellera +detelleront +detendait +detendions +detendons +detendrions +detenions +detentrices +detergeait +detergeates +deterger +detergerions +deteriorais +deteriorat +deteriorerai +determinez +deterrages +deterrasses +deterrent +deterreriez +deterriez +detestable +detestasse +detestee +detesterent +detestons +detheiner +detiennent +detinssiez +detirant +detiree +detirerent +detiriez +detonante +detonates +detoneraient +detones +detonnait +detonnates +detonnerent +detonnons +detordez +detordre +detordus +detortillait +detortille +detortillons +detourant +detouree +detourerent +detourna +detournasses +detournent +detourneriez +detours +detoxifias +detoxifie +detoxifieras +detoxifiions +detoxiquasse +detoxiquent +detractai +detracterai +detracteront +detractrices +detraquerez +detrempa +detrempasses +detrempera +detremperons +detricotage +detricoter +detritique +detrompais +detrompat +detromperais +detrompeur +detronait +detronates +detronerait +detroniez +detroquames +detroque +detroqueras +detroquions +detroussasse +detruiras +detruisait +detruisis +detruite +deuils +deuteron +deuxiemement +devalaient +devalassions +devaleraient +devales +devalisant +devalisee +devaliserent +devaliseuses +devalorisent +devaluaient +devaluera +devaluerons +devancai +devancassiez +devancer +devancerions +devancions +devariait +devariates +devarierait +devariiez +devasas +devasees +devaseras +devasions +devastasse +devastations +devastes +developpai +developpees +developpeurs +devenant +deventais +deventat +deventerais +deventez +deverbales +deverdir +deverdirions +deverdisses +devergondai +devergondez +deverguant +deverguee +deverguerent +deverguons +devernirait +devers +deversassent +deversements +deverserez +deversoirs +devetent +devetirais +devetisse +devetue +deviames +deviasses +devidais +devidat +deviderais +devideur +devie +deviendriez +deviera +deviergeai +deviergerai +deviergeront +deviez +devinait +devinates +devinerait +devines +devinmes +devintes +devirasse +devirent +devireriez +devirilisai +devirilisera +devisage +devisager +devisait +devisates +deviserait +devisiez +devissais +devissat +devisserais +devissez +devitalisant +devitalise +devitalisons +devitrifie +devitrifions +devoieras +devoilais +devoilat +devoilerai +devoileront +devoisees +devoltant +devoltee +devolterent +devoltiez +devolutions +devorai +devorasse +devoratrices +devorerait +devoreurs +devotes +devots +devouassent +devouements +devouerez +devoya +devoyasses +devoyes +devras +devrillames +devrille +devrilleras +devrillions +dewattes +dextrorse +dezinguaient +dezingues +dezippames +dezippe +dezipperas +dezippions +dezonant +dezonee +dezonerent +dezonons +dezoomassent +dezoomer +dezoomerions +dhimma +diabetiques +diableries +diaboliques +diabolisees +diaboliserez +diabolo +diachylum +diaconales +diacritique +diagnostic +dial +dialectaux +dialectisa +dialectisas +dialectisent +dialoguai +dialoguerai +dialogueront +dialypetales +dialysassent +dialysepale +dialyserez +dialysions +diamantaient +diamantasses +diamantera +diamanterons +diamantins +diametres +diamorphines +diapedese +diaphorese +diaphragmant +diaphragmee +diaphysaire +diapositive +diaprasse +diaprent +diapreriez +diaprures +diascope +diastase +diathermane +diathermique +diatomite +diaules +dibasiques +dicarbonyles +dicetones +dichotomes +dichromates +dicos +dictais +dictasses +dictatoriaux +dictera +dicterons +didacticiel +didactismes +diductions +diegetiques +dienes +diesa +diesasses +dieselisai +dieseliser +diesels +dieserez +diester +dietetiques +diffamaient +diffamassent +diffamatoire +diffamerai +diffameront +differais +differat +differentiai +differerai +differeront +difficultes +diffluant +diffluence +diffluerais +diffluez +diffractes +diffus +diffusante +diffusates +diffuserais +diffuseur +digera +digerasses +digerera +digererons +digesteur +digestive +digitalines +digitalisas +digitalisee +digitigrades +digne +digon +digressait +digressates +digresserent +digressifs +dihydrogene +dilacerai +dilacerer +dilapidaient +dilapidee +dilapiderent +dilapidons +dilatames +dilatassions +dilatee +dilaterent +dilations +dilemmes +diligentai +diligenterai +diluais +diluasses +diluera +diluerons +dilutifs +diluvienne +dimensionnai +dimensionner +dimetrodon +diminuasse +diminuendo +diminuerent +diminuons +dimissoriale +dinaient +dinant +dinates +dindonnait +dindonnates +dindonnes +dine +dinerez +dineurs +dinghys +dinguas +dinguer +dingueries +diniez +dinornis +diocesaine +dioecies +dionees +dioptrie +diot +dipetale +diphterie +diphtonguait +diphtongue +diphtonguons +diploidie +diplomantes +diplomates +diplomera +diplomerons +diplopies +dipneustes +dipsacees +diptyques +direct +directoire +directs +dirigea +dirigeantes +dirigee +dirigeras +dirigions +dirlo +disais +discaux +discernas +discernees +discerneras +discernions +disciplinat +discipliniez +discoidal +discomptait +discomptates +discompteurs +discontinua +discontinuat +disconvenus +disconvienne +discophilies +discordant +discordat +discorderas +discordions +discountai +discounterai +discouraient +discoureuses +discourrait +discourtoise +discourusses +discreditait +discredite +discreditons +discretisai +discretisera +discriminai +discriminera +disculpait +disculpates +disculpes +discussion +discutasses +discutera +discuterons +discutons +disettes +disgracia +disgraciera +disharmonie +disjoignes +disjoindras +disjointes +disjonctasse +disjonctent +disjonctifs +disloquaient +disloques +disparait +disparitions +disparussiez +dispatchant +dispatchee +dispatcheuse +dispendieux +dispensames +dispensateur +dispenser +dispersaient +dispersas +dispersee +disperserait +dispersiez +dispo +disposait +disposassiez +disposerai +disposeront +disputaille +disputames +dispute +disputeras +disputions +disqualifiai +disquates +disquerait +disquettes +disruptions +dissection +disseminait +disseminates +dissemines +dissequa +dissequasses +dissequera +dissequerons +dissequons +dissertasse +dissertent +disserteriez +disses +dissimulasse +dissimules +dissipait +dissipates +dissiperait +dissipiez +dissociais +dissociat +dissocierai +dissocieront +dissolubles +dissolvais +dissolviez +dissonances +dissonerait +dissoniez +dissoudrez +dissuadai +dissuaderai +dissuaderont +dissuasive +distal +distancas +distancees +distancerais +distancez +distanciees +distancierez +distancons +distendant +distendisse +distends +distillai +distillats +distillerait +distilles +distinctifs +distingues +distomatoses +distordes +distordras +distordues +distractions +distrais +distrayante +distribuai +distribuerai +distributif +dite +diuretique +divagations +divaguas +divaguer +divaguerions +divalent +divergeai +divergeons +divergeriez +diversement +diversifient +diversion +diverties +divertirait +divertissais +divertisses +dividende +divinatrices +divinisant +diviniserait +divinisiez +divisais +divisat +diviserais +diviseur +division +divorcai +divorcassiez +divorcerai +divorceront +divulgation +divulguant +divulguee +divulguerent +divulguons +dixiemement +dizenier +djainismes +djembe +djiboutien +djinn +docetisme +docimologies +doctes +doctorante +doctrinaires +docu +documentasse +documentee +documentons +dodecagonaux +dodecastyles +dodelinas +dodelinees +dodelineras +dodelinions +dodinant +dodinee +dodinerent +dodinons +dogaresse +dogmatiques +dogmatiserai +dogmatiste +doguines +doigtas +doigtees +doigterez +doigtions +doives +dolait +dolates +doleaux +doleraient +doles +dolines +dollarisames +dollarisez +doloire +dolomites +dolos +domaines +domes +domestiquais +domiciliait +domiciliera +domiens +dominante +dominates +dominent +domineriez +dominicaine +dominiquais +dominotieres +dompta +domptant +domptee +dompterent +dompteuses +donas +donatistes +dondons +donjuanisa +donjuanisiez +donnames +donnassions +donneraient +donnes +dons +dopai +dopante +dopates +doperait +dopeurs +dopplers +dorait +dorates +dorera +dorerons +doriennes +dorlotai +dorlotassiez +dorloter +dorloterions +dormait +dormeur +dormions +dormiriez +dormit +doronics +dosable +dosas +dosees +doserez +doseuse +dosions +dossieres +dotal +dotasses +dotees +doterez +dots +douaisienne +douance +douars +doubienne +doublais +doublasses +doublement +doublerent +doublette +doublions +doublonnas +doublonner +doublures +douceurs +douchanbeens +douche +doucheras +doucheur +douci +doucirai +douciront +doucisses +doudoune +douera +douerons +douillaient +douilles +douillions +doum +douros +doutas +doutees +douterez +douteuses +douvelles +douziemes +doyens +drachat +draconiennes +drageifiai +drageifierai +drageonnage +drageonner +draglines +dragster +draguasse +draguent +dragueriez +draguiez +draieras +draina +drainantes +draine +draineras +draineuse +draisines +dramatisai +dramatisasse +dramatisee +dramatisons +drap +drapassent +drapees +draperas +drapez +dravasse +dravent +draveriez +dravidien +drayage +drayassent +drayer +drayerions +drayoirs +dreiges +dressai +dressasses +dressera +dresserons +dressings +dreyfusards +dribblas +dribblees +dribblerez +dribblez +drillai +drillassiez +drillerai +drilleront +drink +drivant +drivee +driverent +drivez +droguait +droguates +droguerait +drogues +droides +droitisai +droitiser +droitiste +droleries +drome +drontes +dropasse +dropent +droperiez +droppage +droppassent +dropper +dropperions +droseracee +drossais +drossat +drosserais +drossez +drue +druidisme +drummeuses +drupes +dualisait +dualisates +dualises +dualites +dubitation +dubs +ducats +ducs +dudgeonnes +duel +duettos +duitages +duitasses +duitera +duiterons +dulcicole +dulcifiasse +dulcifiee +dulcifierent +dulcifions +dumper +dunette +duodenite +dupai +dupassiez +duperai +duperions +dupions +duplexant +duplexee +duplexerent +duplexons +duplicatifs +dupliquais +dupliquat +dupliquerais +dupliquez +duquames +duque +duquerait +duquiez +durai +durales +durassent +duraux +durcirait +durcissais +durcissez +durent +dureriez +durian +durs +dutes +duveta +duvetasses +duvetera +duveterons +duvets +duvetteriez +dyarchie +dynamique +dynamisante +dynamisates +dynamises +dynamitage +dynamiter +dynamiteries +dynamitez +dynaste +dysacousie +dysbarisme +dyscalculie +dyscrasies +dysenterie +dyslogies +dysmnesie +dyspeptiques +dysphonies +dysplasiques +dysprosodies +dysthymies +dystoniques +dzeta +ebahimes +ebahiriez +ebahissement +ebarba +ebarbasse +ebarbent +ebarberiez +ebarbiez +ebattais +ebattirent +ebattra +ebattrons +ebaubir +ebaubirions +ebaubisses +ebauchai +ebauchassiez +ebaucherai +ebaucheront +ebauchons +ebaudirait +ebaudissais +ebaudit +ebavurant +ebavuree +ebavurerent +ebavurons +ebenistes +eberluassent +eberluer +eberluerions +ebisela +ebiselasses +ebiseles +ebisellerait +eblouie +eblouirent +eblouissant +eblouissiez +eborgnai +eborgnassiez +eborgner +eborgnerions +ebouages +ebouasses +ebouera +ebouerons +eboulant +eboulee +eboulerait +ebouleuses +ebourgeonnat +ebouriffais +ebouriffera +ebourrais +ebourrat +ebourrerais +ebourrez +eboutaient +eboutassions +ebouteraient +eboutes +ebranchais +ebranchat +ebrancherai +ebrancheront +ebranlaient +ebranlera +ebranlerons +ebrasais +ebrasat +ebraserai +ebraseront +ebrechaient +ebrechera +ebrecherons +ebrietes +ebriquant +ebriquee +ebriquerent +ebriquons +ebrouames +ebroue +ebrouerais +ebrouez +ebruitant +ebruitee +ebruiterait +ebruitiez +eburneens +ecachas +ecachees +ecacherez +ecailla +ecaillasse +ecaillent +ecailleres +ecailleuse +ecalai +ecalassiez +ecalerai +ecaleront +ecangages +ecanguasse +ecanguent +ecangueriez +ecanguiez +ecarquillee +ecart +ecartassent +ecartelai +ecarteler +ecartement +ecarterent +ecartiez +ecchymotique +echafaud +echafaudas +echafaudees +echafauderez +echafauds +echalassera +echalotee +echancrant +echancree +echancrerent +echancrons +echangeait +echangeates +echangerait +echangeurs +echanson +echappa +echappasse +echappee +echapperait +echappiez +echardonnais +echarnant +echarnee +echarnerait +echarneuses +echarognai +echarognerai +echarpait +echarpates +echarperait +echarpiez +echassier +echauda +echaudasse +echaudement +echauderent +echaudoir +echauffante +echauffates +echauffes +echauguettes +echaumassent +echaumer +echaumerions +echeances +echees +echelonnera +echenillages +echenillera +echenilloir +echeras +echeveaux +echevelerent +echevellerai +echevelles +echevinat +echiffa +echiffasses +echiffera +echifferons +echinai +echinassiez +echinerai +echineront +echinocoque +echiquete +echographiai +echoirait +echoppa +echoppasses +echoppera +echopperons +echosondeur +echouai +echouassiez +echouer +echouerions +echues +ecimai +ecimassiez +ecimerai +ecimeront +eclaboussait +eclabousse +eclaboussiez +eclafais +eclafat +eclaferais +eclafez +eclairagiste +eclairants +eclaircie +eclaircirent +eclaircissez +eclairements +eclairerez +eclairez +eclanches +eclatantes +eclate +eclaterais +eclateur +eclectismes +eclipsas +eclipsees +eclipserez +ecliptique +eclissant +eclissee +eclisserent +eclissons +eclorais +eclosant +eclosoir +eclusait +eclusates +ecluserait +eclusier +ecobilan +ecobuant +ecobuee +ecobuerent +ecobuons +ecoeurante +ecoeurera +ecoeurerons +ecographies +ecolages +ecologie +ecologues +econduire +econduisant +econduisisse +econduites +econometrie +economisai +economiserai +economiste +ecopant +ecopates +ecoperait +ecopes +ecoproduits +ecorcames +ecorce +ecorceras +ecorceuse +ecorchames +ecorche +ecorcherais +ecorcheront +ecorchure +ecornai +ecornassiez +ecornerai +ecorneront +ecorniflant +ecorniflee +ecosphere +ecossait +ecossates +ecosserait +ecosseurs +ecota +ecotasse +ecotee +ecoterent +ecoteuses +ecotoxicites +ecoula +ecoulasses +ecoulent +ecouleriez +ecoumenes +ecourtas +ecourtees +ecourterez +ecourtichais +ecoutais +ecoutasses +ecoutera +ecouterons +ecoutilles +ecouvillons +ecrapouti +ecrapoutiras +ecrasa +ecrasas +ecrasees +ecraseras +ecraseuse +ecremaient +ecremassions +ecremeraient +ecremes +ecremons +ecretas +ecretees +ecreteras +ecretez +ecriait +ecriates +ecrierait +ecriiez +ecriras +ecriteau +ecrivaillai +ecrivains +ecrivassant +ecrivassent +ecrivassiez +ecrivirent +ecrou +ecrouassent +ecrouent +ecroueriez +ecrouimes +ecrouirez +ecrouissait +ecrouites +ecroulassent +ecroulements +ecroulerez +ecrouons +ecroutant +ecroutee +ecrouterent +ecroutiez +ecstasies +ectinites +ectothermes +ecuelle +ecuissant +ecuissee +ecuisserent +ecuissons +eculassent +eculer +eculerions +ecumages +ecumas +ecumees +ecumerez +ecumeux +ecurai +ecurassiez +ecurerai +ecureront +ecuries +ecussonnes +ecuyeres +edaphiques +edentait +edentates +edenterait +edentiez +edictas +edictees +edicterez +edictons +edifiante +edifiates +edifiee +edifierent +edifions +edit +editassent +editer +editerions +editionnai +editionnerai +editons +edits +edredons +edulcorais +edulcorasses +edulcorent +edulcoreriez +eduquai +eduquassiez +eduquerai +eduqueront +efaufilait +efaufilates +efaufilerait +efaufiliez +effacai +effacassiez +effacer +effacerions +effacure +effanant +effanee +effanerent +effaniez +effarames +effarassions +effarera +effarerons +effarouchais +effarouchez +effectif +effectuai +effectuerai +effectueront +effeminait +effeminates +effemines +efferents +effeuillage +effeuillas +effeuillees +effeuilleras +effeuillez +efficiences +effilai +effilassiez +effiler +effilerions +effilions +effilochas +effilochees +effilocheras +effilocheuse +effilures +efflanquer +effleurages +effleurasses +effleurent +effleureriez +effleurimes +effleurirez +effleurisse +effloraison +effluent +effluvant +effluvent +effluveriez +effondrai +effondrer +efforcai +efforcassiez +efforcerai +efforceront +effraie +effraierions +effrangeames +effrangee +effrangerais +effrangez +effrayante +effrayates +effrayerait +effrayiez +effritais +effritat +effriterai +effriteront +effrontees +effusion +egaie +egaierez +egaillait +egaillates +egaillerait +egailliez +egalait +egalates +egalerais +egalez +egalisas +egalisation +egaliserai +egaliseront +egalitarisme +egarais +egarat +egarer +egarerions +egayai +egayasse +egayement +egayerent +egayons +egermais +egermat +egermerais +egermez +eglefin +eglomisait +eglomisates +eglomises +egocentriste +egoistes +egorgeasse +egorgements +egorgerent +egorgeuses +egosillant +egosillee +egosillerent +egosillons +egoutier +egouttais +egouttat +egoutterai +egoutteront +egraina +egrainasse +egrainement +egrainerent +egrainons +egrappas +egrappees +egrapperez +egrappoirs +egratignasse +egratignent +egratigniez +egrenage +egrenassent +egrenements +egrenerez +egrenions +egrisages +egrisasses +egrisera +egriserons +egrotants +egrugeant +egrugees +egrugerait +egrugiez +egueulasse +egueulement +egueulerent +egueulons +ehonte +eidetismes +ejaculaient +ejaculerais +ejaculez +ejectais +ejectat +ejecterais +ejecteur +ejectons +ejointait +ejointates +ejointerait +ejointiez +elaborames +elaboration +elaborerais +elaborez +elaguais +elaguat +elaguerais +elagueur +elamite +elancas +elancees +elanceras +elancions +elargie +elargirent +elargissant +elargissions +elastomere +elbeuf +eleatique +elective +electricien +electrifiait +electrisais +electrisent +electrochocs +electronique +electrophore +electuaire +elegiaque +elegirait +elegissais +elegit +eleometres +elephantin +elevai +elevassiez +elevatrice +eleverais +eleveur +elfes +eliassent +elidait +elidates +eliderait +elidiez +elieraient +elies +elimaient +elimassions +elimeraient +elimes +eliminas +elimination +eliminer +eliminerions +elinde +elinguant +elinguee +elinguerent +elinguons +elire +elision +elitiste +elliptiques +elogieux +eloignassent +eloignements +eloignerez +eloise +elongeames +elongee +elongeras +elongions +eloxees +eluames +eluates +elucidassent +elucidees +eluciderez +elucubra +elucubrasses +elucubrent +elucubreriez +eludai +eludassiez +eluderai +eluderont +eluer +eluerions +elus +elut +elyme +elzevirienne +emaciant +emaciations +emacieraient +emacies +emaillaient +emaillerons +emaillons +emanant +emanations +emancipas +emancipation +emanciperai +emanciperont +emaner +emanerions +emargeai +emargeassiez +emarger +emargerions +emasculais +emasculat +emasculerai +emasculeront +embaclais +embaclat +embaclerais +embaclez +emballaient +emballassent +emballements +emballerez +emballez +embarcation +embardoufler +embarquaient +embarquera +embarquerons +embarrais +embarrassat +embarrassiez +embarrera +embarrerons +embasement +embastillas +embat +embatas +embatees +embaterez +embatirent +embatra +embatrons +embattent +embattisses +embattrait +embattues +embauchaient +embauches +embauchons +embaumassent +embaumements +embaumerez +embaumez +embecquant +embecquee +embecquerent +embecquons +embeguiner +embellies +embellirez +embellisse +embellissiez +embetames +embetassions +embetera +embeterons +embeurrais +embeurrat +embeurrerais +embeurrez +emblavaient +emblavera +emblaverons +emblematique +embobelinant +embobelinees +embobinai +embobinerai +embobineront +emboirait +emboita +emboitant +emboitee +emboiterait +emboitiez +embolie +embolus +embossames +embosse +embosseras +embossions +embouait +embouates +emboucaner +embouchaient +embouches +embouee +embouerent +embouons +embouquerez +embourba +embourbasses +embourbera +embourberons +embourrer +embout +emboutassent +embouterai +embouteront +emboutira +emboutirons +emboutissent +embrancha +embranchent +embraquai +embraquerai +embraqueront +embrasait +embrasates +embrases +embrassais +embrassasses +embrassent +embrasseriez +embrassiez +embrayais +embrayat +embrayerais +embrayeur +embrevait +embrevates +embreves +embrigadames +embrigade +embrigadez +embringuant +embringuee +embringuons +embrochames +embroche +embrocherais +embrochez +embronchant +embronchee +embronchiez +embrouillez +embrumames +embrume +embrumeras +embrumions +embryon +embryoscopie +embuais +embuat +embuchasse +embuchent +embucheriez +embuee +embuerent +embugnaient +embugnes +embus +embusquas +embusquees +embusquerez +embusse +embuvais +emechais +emechat +emecherais +emechez +emendant +emendations +emenderait +emendiez +emergeait +emergeates +emergents +emergerez +emerillon +emerisais +emerisat +emeriserais +emerisez +emerveilla +emerveiller +emetique +emettant +emettra +emettriez +emeutiere +emiait +emiates +emierait +emiettai +emiettassiez +emietter +emietterions +emiez +emigrants +emigrations +emigrerait +emigrettes +emiliens +emincasse +emincent +eminceriez +eminence +emirat +emiriens +emissiez +emites +emmagasinas +emmailla +emmaillasses +emmaillera +emmaillerons +emmaillotais +emmaillotez +emmanchant +emmanchee +emmancherait +emmanchiez +emmelais +emmelat +emmelerai +emmeleront +emmenagera +emmenagerons +emmenais +emmenat +emmenerais +emmenez +emmerdaient +emmerdassent +emmerdements +emmerderez +emmerdez +emmetrant +emmetree +emmetrerent +emmetrons +emmiellames +emmielle +emmielleras +emmiellions +emmitonnasse +emmitonnent +emmitouflai +emmitouflez +emmurasse +emmurement +emmurerent +emmurons +emonctions +emondames +emonde +emonderas +emondeuse +emorfilages +emorfilasses +emorfilera +emorfilerons +emotifs +emotionnant +emotionnat +emotionner +emotives +emottames +emotte +emotterais +emotteur +emouchai +emouchassiez +emoucherai +emoucheront +emouchetames +emouchete +emouchets +emoud +emoudrions +emoule +emoulons +emoulussiez +emoussais +emoussat +emousserai +emousseront +emoustillait +emoustillez +emouvants +emouvrait +empaffes +empaillas +empaillees +empailleras +empailleuse +empalait +empalates +empaleraient +empales +empalmait +empalmates +empalmerait +empalmiez +empanachames +empanache +empanacheras +empanachions +empannait +empannates +empannerait +empanniez +empaquetait +empaquetates +emparaient +emparassions +empareraient +empares +empatames +empate +empaterais +empatez +empattaient +empattera +empatterons +empaumai +empaumassiez +empaumerai +empaumeront +empechaient +empechera +empecherons +empechons +empeguassent +empeguer +empeguerions +empenage +empennant +empennee +empennelant +empennelee +empennellera +empennelles +empennerent +empennons +emperlas +emperlees +emperlerez +empesa +empesasse +empesent +empeseriez +empestai +empestassiez +empesterai +empesteront +empetrait +empetrates +empetres +empiegeant +empiegees +empiegerent +empierra +empierrasses +empierrent +empierreriez +empietai +empietassiez +empieterai +empieteront +empiffrait +empiffrates +empiffrerait +empiffriez +empilai +empilassiez +empiler +empilerions +empilions +empirasse +empirent +empireriez +empiriques +emplafonnat +emplafonniez +emplie +emplirent +emplissais +emplit +emploieras +employasse +employer +employons +emplumassent +emplumer +emplumerions +empochaient +empoches +empoignais +empoignasses +empoignera +empoignerons +empoise +empoisonnee +empoissait +empoissates +empoisserait +empoissiez +empoissonnes +emporta +emportasses +emportent +emporteriez +emposieus +empotant +empotee +empoterait +empotiez +empourpras +empourprees +empourprerez +empoussiera +empoussierat +empreignez +empreindre +empreints +empresserez +empresura +empresurasse +empresurent +emprises +emprisonnent +empruntai +emprunterai +emprunteront +emprunts +empuantirait +emulait +emulates +emulera +emulerons +emulsifia +emulsifie +emulsifieras +emulsifiions +emulsionnera +emurent +emydes +enamourant +enamouree +enamourerent +enamourons +enarchies +enarthroses +encabanas +encabanees +encabanerez +encablure +encadrantes +encadre +encadrerais +encadreur +encageai +encageassiez +encager +encagerions +encagoulais +encagoulat +encagoulez +encaissaient +encaisserez +encaissions +encanaillais +encanaillez +encantames +encante +encanteras +encanteuse +encapsulera +encaquassent +encaquements +encaquerez +encart +encartas +encartees +encarterez +encartions +encartonnera +encartouches +encasernas +encasernees +encasernerez +encastela +encastelera +encastrable +encastrerez +encaustiqua +encaustiquez +encavait +encavates +encaveraient +encaves +enceignais +enceignirent +enceindra +enceindrons +enceintant +enceintee +enceinterent +enceintons +encellulasse +encellulons +encensasse +encensement +encenserent +encenseuses +encephales +encephaloide +encerclas +encerclees +encercleras +encerclions +enchainasse +enchainement +enchainerent +enchainons +enchantez +enchassaient +enchassera +enchasserons +enchatelai +enchatelerai +enchatonnait +enchatonne +enchatonniez +enchaussas +enchaussees +enchausserez +enchemisa +enchemisasse +enchemisent +encheres +encherirait +encherissais +enchevaucha +enchevauchat +enchevetrant +enchevetrees +enchevetrons +enchifrenas +enchilada +enclavant +enclavee +enclaverait +enclaviez +enclenchas +enclenchees +enclencheras +enclenchez +encliquetez +enclora +enclorons +enclosons +enclouait +enclouates +enclouerait +enclouiez +enclumettes +encochas +encochees +encocheras +encochions +encodant +encodee +encoderent +encodeuses +encofframes +encoffre +encoffreras +encoffrions +encollait +encollates +encollerait +encolleurs +encombrai +encombrasse +encombrement +encombrerent +encombrons +encordas +encordees +encorderez +encore +encornassent +encorner +encornerions +encornure +encoublasse +encoublent +encoubleriez +encouragea +encourageas +encouraient +encourir +encourrions +encourus +encrages +encrassaient +encrassera +encrasserons +encree +encrerent +encreuses +encrons +encroutames +encroute +encrouterais +encroutez +encryptait +encryptates +encrypterait +encryptiez +encuvames +encuve +encuverais +encuvez +endemies +endentais +endentat +endenterai +endenteront +endettait +endettates +endettes +endeuillames +endeuille +endeuilleras +endeuillions +endevasse +endevera +endeverons +endiablais +endiablat +endiablerais +endiablez +endigua +endiguasses +endiguent +endigueriez +endimanchai +endimanchez +endisquant +endisquee +endisquerent +endisquons +endocraniens +endoctrinent +endodermique +endoge +endolorir +endometriome +endommageais +endommages +endophasie +endoreisme +endorment +endormiez +endormirent +endormissent +endort +endosmose +endossait +endossataire +endossera +endosserons +endossures +enduirais +enduisage +enduisiez +enduisit +endurai +endurants +endurcie +endurcirent +endurcissant +endurcit +endurerais +endurez +enema +energie +energivores +enervante +enervates +enervera +enerverons +enfaitais +enfaitat +enfaiter +enfaiterions +enfant +enfantassent +enfantements +enfanterez +enfantin +enfargeaient +enfarges +enfarinant +enfarinee +enfarinerent +enfarinons +enfermasse +enfermement +enfermerent +enfermons +enferrassent +enferrer +enferrerions +enfeus +enfichait +enfichates +enficherait +enfichiez +enfiellas +enfiellees +enfiellerez +enfievra +enfievrasses +enfievrent +enfievreriez +enfilade +enfilas +enfilees +enfileras +enfileuse +enfirouapais +enflammaient +enflammes +enflassiez +enfler +enflerions +enfleurait +enfleurates +enfleurerait +enfleuriez +enfoiree +enfoncas +enfoncees +enfonceras +enfonceuse +enforcies +enforcirez +enforcisse +enfouie +enfouirent +enfouissant +enfouissions +enfourchant +enfourchee +enfourchiez +enfournais +enfournat +enfournerai +enfourneront +enfreignions +enfreignons +enfuient +enfuirent +enfuissiez +enfumait +enfumates +enfumerait +enfumiez +enfutailla +enfutailles +enfutasse +enfutent +enfuteriez +enfuyais +engage +engageants +engagees +engagerait +engagiez +engainantes +engaine +engaineras +engainions +engamasse +engament +engameriez +enganes +engazonnerez +engeance +engendrant +engendree +engendrerait +engendriez +engerbames +engerbe +engerberas +engerbions +engloba +englobasses +englobera +engloberons +engloutimes +engloutiriez +engluage +engluassent +engluements +engluerez +engoba +engobasse +engobent +engoberiez +engommage +engommassent +engommer +engommerions +engoncaient +engoncera +engoncerons +engorgeaient +engorgera +engorgerons +engouait +engouates +engoueraient +engoues +engouffrasse +engouffrons +engourdir +engrais +engraissas +engraissees +engraisseras +engraisseuse +engrangeai +engranger +engravais +engravat +engraverais +engravez +engrelure +engrenant +engrenee +engrenerait +engreneurs +engrossa +engrossasses +engrossera +engrosserons +engrumelais +engrumelat +engrumelions +engueulais +engueulat +engueulerai +engueuleront +enguirlande +enhardi +enhardiras +enhardissait +enhardites +enharnachee +enherba +enherbasses +enherbent +enherberiez +eniemes +enivrait +enivrassiez +enivrer +enivrerions +enjambaient +enjambera +enjamberons +enjambons +enjavelerent +enjeu +enjoigniez +enjoignit +enjoindrez +enjola +enjolasses +enjolent +enjoleriez +enjoliez +enjolivasse +enjolivement +enjoliverent +enjoliveuses +enjouees +enjuguant +enjuguee +enjuguerent +enjuguons +enjuivassent +enjuiver +enjuiverions +enjuponnes +enkikinais +enkikinasses +enkikinera +enkikinerons +enkystais +enkystat +enkysterai +enkysteront +enlacait +enlacates +enlaceraient +enlaces +enlaidimes +enlaidiriez +enleva +enlevasse +enlevement +enleverent +enlevons +enlias +enliasser +enlie +enlieras +enlignai +enlignassiez +enligner +enlignerions +enlions +enlisassent +enlisements +enliserez +enloge +enlogeassent +enlogent +enlogerez +enluminai +enluminerai +enlumineront +enluminure +enneasyllabe +enneigeas +enneigement +enneigeras +enneigez +ennoblimes +ennobliriez +ennoie +ennoierez +ennoyaient +ennoyassions +ennoyiez +ennuageant +ennuagees +ennuagerait +ennuagiez +ennuieras +ennuyaient +ennuyassent +ennuyerent +enol +enoncant +enoncee +enoncerent +enonciation +enorgueillie +enorme +enouai +enouassiez +enouerai +enoueront +enquerez +enquerras +enquetais +enquetat +enqueterais +enqueteur +enquiere +enquillames +enquille +enquilleras +enquillions +enquiquine +enquises +enracinaient +enracinera +enracinerons +enrageaient +enrageassent +enrager +enragerions +enraient +enraierons +enrayames +enraye +enrayerais +enrayez +enregimente +enregistras +enregistrez +enrenant +enrenee +enrenerait +enreniez +enresinas +enresinees +enresineras +enresinions +enrhumasse +enrhument +enrhumeriez +enrichie +enrichirent +enrichissant +enrichissiez +enrobais +enrobasses +enrobent +enroberiez +enrobons +enrochassent +enrochements +enrocherez +enrola +enrolasses +enrolent +enroleriez +enrolons +enrouassent +enrouements +enrouerez +enrouilles +enroulaient +enroulera +enroulerons +enroulons +enrubannant +enrubannee +enrubannons +ensablassent +ensablements +ensablerez +ensacha +ensachasse +ensachent +ensacheriez +ensachiez +ensaisinas +ensaisinees +ensaisineras +ensaisinions +ensanglantez +ensauvagees +ensauvagiez +enseignant +enseignat +enseignerai +enseigneront +ensellements +ensembliste +ensemencasse +ensemencons +enserrassent +enserrer +enserrerions +enseveli +enseveliras +ensevelissez +ensilages +ensilasses +ensilera +ensilerons +ensimage +ensimassent +ensimer +ensimerions +ensoleillat +ensoleilles +ensorcelai +ensorcelasse +ensorceler +ensorcelle +ensoufrait +ensoufrates +ensoufrerait +ensoufriez +ensoutanames +ensoutane +ensoutaneras +ensoutanions +ensuifant +ensuifee +ensuiferent +ensuifons +ensuivies +ensuqua +ensuquasses +ensuquera +ensuquerons +entablaient +entablera +entablerons +entachai +entachassiez +entacherai +entacheront +entaillages +entaillasses +entaillera +entaillerons +entamai +entamassiez +entamerai +entameront +entartage +entartassent +entarter +entarterions +entartions +entartrant +entartree +entartrerait +entartriez +entassant +entassee +entasserait +entassiez +entelle +entende +entendions +entendons +entendrions +entenebres +enterai +enterina +enterinasses +enterinent +enterineriez +enterique +enterokinase +enterrage +enterrassent +enterrements +enterrerez +entes +entetants +entetee +enteterait +entetiez +enthousiasma +enticha +entichasses +entichent +enticheriez +entiere +entoila +entoilasse +entoilent +entoileriez +entoirs +entolas +entolees +entolerez +entolez +entonnages +entonnasse +entonnement +entonnerent +entonnoir +entortiller +entourages +entourasses +entourera +entourerons +entournure +entraccuser +entraccusez +entradmirant +entradmirera +entradmiriez +entraidiez +entrainaient +entrainerez +entrainez +entrante +entrapercois +entrapercus +entrasses +entravames +entrave +entraveras +entravions +entrebat +entrebattrez +entrechats +entrechoquez +entrecoupat +entrecoupiez +entrecroisas +entrecroiser +entree +entregent +entregorgee +entrejeux +entrelacerez +entrelacs +entrelardera +entremelerez +entremet +entremirent +entremites +entrenuire +entrenuisent +entrenuisons +entreposait +entreposates +entreposeurs +entreprenais +entreprends +entreprenne +entrerais +entresolee +entretailler +entretenant +entretenu +entretinmes +entretoile +entretuaient +entretuee +entrevirent +entrevoient +entrevoutait +entrevoute +entrevoutons +entrevue +entroblige +entrobligent +entropique +entrouvrait +entrouvrira +entrouvrons +entubas +entubees +entuberez +enturbanne +enucleait +enucleates +enuclees +enumerais +enumerat +enumerees +enumererez +enuqua +enuquasses +enuquera +enuquerons +enuretiques +envahirait +envahissais +envahisses +envahites +envasassent +envasements +envaserez +enveloppa +enveloppas +enveloppees +envelopperas +enveloppions +envenimasse +envenimee +envenimerait +envenimiez +envergeant +envergees +envergerent +envergiez +enverguasse +enverguent +envergueriez +envergures +enverriez +enviaient +enviassions +envidames +envide +envideras +envidions +envierais +envieuse +envines +environnante +environnates +environnerai +envisagera +envisagerons +envoila +envoilasses +envoilera +envoilerons +envolai +envolassiez +envoler +envolerions +envoutai +envoutasse +envoutement +envouterent +envouteuses +envoyames +envoye +envoyez +enzymologie +eohippus +eoliques +eosinophiles +epagneule +epaillames +epaille +epailleras +epaillions +epaissie +epaissirent +epaississant +epamprai +epamprassiez +epamprer +epamprerions +epancha +epanchasses +epanchent +epancheriez +epanchons +epandes +epandis +epandrai +epandront +epannait +epannates +epannelait +epannelates +epannelle +epannerais +epannez +epanouirai +epanouiront +epar +epargnante +epargnates +epargnerait +epargniez +eparpillas +eparpillees +eparpilleras +eparpillez +eparts +epatant +epatat +epaterai +epateront +epaufra +epaufrasses +epaufrera +epaufrerons +epaulai +epaulassent +epaulements +epaulerez +epaulieres +epecla +epeclasses +epeclera +epeclerons +epees +epelames +epele +epellations +epelleriez +epenthetique +epepinasse +epepinent +epepineriez +eperdue +eperonnais +eperonnat +eperonnerais +eperonnez +epervins +epeurants +epeuree +epeurerent +epeurons +ephelides +ephesiens +epia +epiait +epiates +epicames +epicasses +epicene +epicera +epiceriez +epicieres +epicrane +epicuriennes +epicycloide +epidemique +epididyme +epie +epieras +epierrai +epierrassiez +epierrer +epierrerions +epierrions +epigamique +epigeneses +epiglottique +epigramme +epigyne +epilant +epilateurs +epilepsies +epileriez +epiliez +epiloguait +epiloguates +epiloguerait +epiloguiez +epinaies +epinasses +epincais +epincat +epincelames +epincele +epinceleras +epincelions +epinceraient +epinces +epincetas +epincetees +epincetions +epincetterez +epincez +epinerai +epineront +epineux +epinglant +epinglee +epinglerent +epinglettes +epinier +epinons +epiphysaires +epiploons +episclerite +episodique +epissait +epissates +episserait +epissiez +epistasies +epistolieres +epitexte +epitheliome +epitome +epivardait +epivardates +epivarderait +epivardiez +eploierai +eploies +eployames +eploye +eplucha +epluchasse +epluchent +eplucheriez +eplucheuses +epoches +epointait +epointates +epointes +epongeages +epongeasses +epongera +epongerons +epontillages +epontillera +eponymies +epouillais +epouillat +epouillerais +epouillez +epoulardait +epoulardates +epoulardiez +epoumonas +epoumonees +epoumonerez +epousa +epousassent +epouser +epouserions +epousseta +epoussetasse +epousseter +epoustoufla +epoustouflas +epoustoufles +epoutiait +epoutiates +epoutierait +epoutiiez +epoutiras +epoutissait +epoutites +epouvantames +epouvante +epouvantez +epoxys +epreignimes +epreignites +epreindriez +eprenais +eprendre +eprenne +eprises +eprouvai +eprouvasse +eprouvent +eprouveriez +eprouvons +epucant +epucee +epucerent +epucons +epuisante +epuisates +epuiseraient +epuises +epulies +epuraient +epurassions +epuratives +epurera +epurerons +equanimite +equarrirais +equarrissage +equarrisses +equarrites +equatorienne +equerrames +equerre +equerreras +equerrions +equeutait +equeutates +equeuterait +equeutiez +equidistante +equilibrage +equilibrants +equilibreurs +equilibrons +equin +equinoxiaux +equipant +equipates +equipera +equiperons +equipole +equipons +equisetale +equitante +equivalaient +equivalez +equivalus +equivaux +equivoquerai +eradicable +eradiquaient +eradiques +eraflames +erafle +eraflerais +eraflez +eraillait +eraillates +erailles +erasmiens +erecteur +ereinta +ereintantes +ereinte +ereinterais +ereinteur +eremitique +eresipele +erevanaises +ergographe +ergometrique +ergosterol +ergotames +ergotat +ergoterais +ergoteront +ergots +erigeames +erigee +erigeras +erigiez +eristique +eroda +erodasses +erodera +eroderons +erogenes +erotiques +erotisants +erotisations +erotiserait +erotisiez +erpetologies +errames +errasses +erratums +erreras +errez +ersatz +erubescents +eructames +eructation +eructerais +eructez +erugineuse +erythrasmas +esbaudies +esbaudirez +esbaudisse +esbignai +esbignassiez +esbignerai +esbigneront +esbroufait +esbroufates +esbrouferait +esbroufeurs +escabeches +escadronnat +escadronnons +escagassasse +escagassent +escaladai +escaladerai +escaladeront +escalier +escalopasse +escalopent +escaloperiez +escamotable +escamotas +escamotees +escamoterez +escamotez +escarbille +escarpin +escarrifiait +eschant +eschates +eschera +escherons +esclaffa +esclaffasses +esclaffera +esclafferons +esclavagea +esclavagerai +esclaves +escoffiai +escoffierai +escoffieront +escomptable +escompter +escomptions +escortant +escortee +escorterent +escortiez +escourgeons +escrimassent +escrimer +escrimerions +escrimions +escroquant +escroquee +escroquerent +escroquiez +eserines +esgourder +eskimo +esoterique +espacant +espacee +espacerait +espaciez +espagnole +esparcet +esperai +esperantiste +esperat +espererais +esperez +espingole +espionnais +espionnat +espionnerais +espionnez +espoirs +espoutis +espringales +esquichames +esquiche +esquicheras +esquichions +esquimaude +esquimautant +esquimauter +esquintaient +esquinter +esquissa +esquissasses +esquissera +esquisserons +esquivais +esquivat +esquiverais +esquivez +essaierais +essaima +essaimasse +essaiment +essaimeriez +essais +essangeant +essangees +essangerent +essanvage +essartames +essarte +essarterais +essartez +essayais +essayat +essayerais +essayeur +esse +essenismes +essentielles +essonnienne +essorait +essorates +essorerait +essoreuses +essorillas +essorillees +essorilleras +essorillions +essouchais +essouchat +essoucherai +essoucheront +essoufflait +essoufflates +essouffles +essuierais +essuyage +essuyassent +essuyerent +est +estafilades +estampais +estampat +estamperais +estampeur +estampilles +estancias +esterifiais +esterifiat +esterifierai +esthesie +estheticien +esthetisais +esthetisent +esthetisons +estimas +estimatif +estimees +estimerez +estiva +estivames +estivassions +estiver +estiverions +estocades +estomaquant +estomaquee +estomaquons +estompas +estompees +estomperas +estompions +estoquaient +estoques +estourbimes +estourbiriez +estradiol +estrapada +estrapadera +estrapassais +estrogene +estropiaient +estropies +estuariens +etablai +etablassiez +etablerai +etableront +etablira +etablirons +etablissent +etagea +etageasses +etageons +etageres +etagiste +etaierais +etains +etalageait +etalageates +etalagerait +etalagiez +etalas +etalees +etaleras +etalez +etalinguait +etalinguates +etalinguiez +etalonnai +etalonner +etalonnions +etamait +etamates +etamera +etamerons +etamines +etampames +etampe +etamperas +etampeur +etamure +etanchasse +etancheifia +etancheifiat +etanches +etanconnait +etanconnates +etanconnes +etape +etarquait +etarquates +etarquerait +etarquiez +etatisa +etatisasses +etatisent +etatiseriez +etatisons +etayaient +etayassions +etayera +etayerons +eteignait +eteignis +eteignons +eteindrions +etend +etendent +etendisse +etendra +etendrons +eternelles +eternisasse +eternisent +eterniseriez +eternites +eternuassent +eternuer +eternuerions +etesiens +etetas +etetees +eteteras +etetions +ethanoate +etheree +etherifias +etherifie +etherifieras +etherifiions +etherisasse +etherisee +etheriserent +etherisme +ethicien +ethique +ethmoidites +ethnicisames +ethnicisez +ethniques +ethnographe +ethnologue +ethologie +ethuses +ethyliques +etiage +etigeait +etigeates +etigerait +etigiez +etincelant +etincelat +etiolames +etiole +etiolerais +etiolez +etiopathes +etiquetais +etiquetat +etiqueteuse +etira +etirant +etiree +etirerait +etireurs +etocs +etoffassent +etoffer +etofferions +etoilaient +etoilassions +etoilera +etoilerons +etolienne +etonnant +etonnat +etonnerai +etonneront +etouffages +etouffas +etouffees +etoufferas +etouffeuse +etoupaient +etoupassions +etouperaient +etoupes +etoupillas +etoupillees +etoupillerez +etoupions +etourdira +etourdirons +etourdisse +etourdites +etrangete +etranglasse +etranglement +etranglerent +etrangleuses +etreci +etreciras +etrecissait +etrecites +etreignimes +etreignites +etreindriez +etrennai +etrennassiez +etrennerai +etrenneront +etrillames +etrille +etrilleras +etrillions +etripant +etripee +etriperent +etripons +etriquassent +etriquer +etriquerions +etrivaient +etrivassions +etriveraient +etrives +etroites +etronconnais +etudiais +etudiasses +etudiera +etudierons +etuvage +etuvassent +etuvements +etuverez +etuvez +etymon +eucaride +euclidiennes +eudemonistes +eues +eugenols +eumes +eupatrides +euphemique +euphemisas +euphemisees +euphemiserez +euphemismes +euphorisais +euphorisent +euphotiques +euploides +euproctes +eurasiens +eurobanque +eurodeputees +eurois +europeanisai +europium +euryhaline +eurythmies +euskara +euskeriennes +eustasies +eutes +euthanasias +euthanasient +eutherien +eutrophique +evacuames +evacuassions +evacuee +evacuerent +evacuons +evadassent +evader +evaderions +evalua +evaluasse +evaluatifs +evaluent +evalueriez +evanescences +evangelisa +evangelisee +evangelismes +evanouir +evanouirions +evaporable +evaporassent +evaporatoire +evaporerais +evaporez +evasait +evasates +evaseraient +evases +evasons +eveillaient +eveilles +eveinage +eventai +eventais +eventat +eventerai +eventeront +eventrait +eventrates +eventres +eventuel +eversions +evertuassent +evertuer +evertuerions +evhemeriste +evidait +evidates +evidente +eviderent +evidoir +evincais +evincat +evincerai +evinceront +eviscerait +eviscerates +evisceres +evitai +evitassiez +eviter +eviterions +evocateur +evolua +evoluasses +evoluera +evoluerons +evoquai +evoquassiez +evoquerai +evoqueront +evulsion +exacerbais +exacerbat +exacerberai +exacerberont +exacteur +exagerais +exagerat +exagerees +exagererent +exagerons +exaltants +exaltations +exalterait +exaltiez +examinait +examinates +examinera +examinerons +exasperai +exasperasse +exasperee +exaspererent +exasperons +exaucassent +exaucements +exaucerez +excava +excavasses +excavatrices +excaverait +excaviez +excedante +excedates +excederai +excederont +excellait +excellates +exceller +excellerions +excentraient +excentrera +excentrerons +excentriques +exceptasse +exceptent +excepteriez +excipa +excipasses +exciperaient +excipes +excisais +excisat +exciserais +exciseur +excitabilite +excitante +excitates +excitatrices +exciterait +excitiez +exclamaient +exclamee +exclamerent +exclamons +excluiez +exclure +exclusion +exclusivites +excommuniat +excommuniez +excoriant +excoriations +excorierait +excoriiez +excretasse +excretent +excreteriez +excretions +excursionnai +excusames +excuse +excuseras +excusions +execrait +execrates +execreraient +execres +executais +executasses +executera +executerons +executions +exedres +exemplarite +exemplifiat +exemplifiez +exemptames +exempte +exempteras +exemption +exercames +exercassions +exerceraient +exerces +exereses +exfiltrant +exfiltrerait +exfiltriez +exfoliante +exfoliates +exfolies +exhalaisons +exhalat +exhalerai +exhaleront +exhaussaient +exhaussera +exhausserons +exhaustifs +exheredaient +exheredera +exherederons +exhibais +exhibat +exhiberais +exhibez +exhilarantes +exhortasse +exhortee +exhorterent +exhortons +exhumassent +exhumees +exhumerez +exige +exigeants +exigees +exigerait +exigibilite +exiguites +exilas +exilees +exilerez +exils +existais +existasses +existerait +existiez +exodes +exogenoses +exondasse +exondee +exonderait +exondiez +exoneras +exonere +exonereras +exonerions +exorbitai +exorbitasse +exorbitent +exorbiteriez +exorcisai +exorciser +exorcisions +exoreique +exotismes +expansibles +expansives +expassas +expassees +expasserez +expat +expatriees +expatrierez +expats +expectorai +expectorasse +expectoree +expectorons +expediassent +expediente +expedierent +expedions +expeditives +experimental +expertisais +expertisat +expertisez +expiais +expiat +expie +expieras +expiions +expirantes +expirateur +expirees +expirerez +explant +explication +explicitant +explicitez +expliquant +expliquee +expliquerent +expliquons +exploitait +exploiter +exploitions +exploras +exploration +explorer +explorerions +explosaient +exploses +explosifs +expo +exportables +exportasses +exporterait +exportiez +exposames +exposassions +exposera +exposerons +exposons +expressos +exprimames +exprime +exprimeras +exprimions +expropriant +expropriat +expropriees +exproprierez +expulsa +expulsasse +expulsent +expulseriez +expulsions +expurgeai +expurgerai +expurgeront +exquisites +exsudais +exsudat +exsudera +exsuderons +extasiai +extasiassiez +extasierai +extasieront +extensibles +extensions +extenuait +extenuassiez +extenuer +extenuerions +exteriorisas +exterioriser +exterminee +exterminons +externalisez +extincteurs +extinguibles +extirpas +extirpation +extirperai +extirperont +extorquait +extorquates +extorquerait +extorqueurs +extournai +extournerai +extourneront +extractives +extradames +extrade +extraderas +extradions +extradures +extrafraiche +extrairais +extrait +extralucides +extraplate +extrapolames +extrapolez +extrarenaux +extrasportif +extravaguent +extravasera +extraverti +extrayions +extremis +extruda +extrudasses +extrudera +extruderons +extrusifs +extubames +extube +extuberas +extubions +exulceraient +exulcerera +exulcererons +exultais +exultat +exulterais +exultez +eyalet +fables +fabricants +fabriquai +fabriquerai +fabriqueront +fabulait +fabulates +fabulent +fabuleriez +fabuliez +facadieres +facetieuses +facettasse +facettent +facetteriez +fachai +fachassiez +facherai +facherions +fachions +facile +facilitante +facilitates +facilitent +faciliteriez +faconde +faconnant +faconnee +faconnerait +faconneurs +faconnons +facticite +factitifs +factoriels +factorisas +factorise +factoriseras +factorisions +factuels +facturas +facture +factureras +facturez +facule +facultes +fadas +fade +faderait +fadets +fado +fagacees +fagota +fagotasse +fagotent +fagoteriez +fagotier +fagoue +faiblesse +faibliras +faiblissait +faiblissions +faienceries +failla +faillasses +faillera +faillerons +faillies +failliras +faillissait +faillites +faineantait +faineantates +faineantise +faisabilites +faisandai +faisander +faisanderies +faisandions +faiseur +faitage +faitout +falacha +falciforme +fallacieuse +falote +falsifias +falsifierai +falsifieront +falunage +falunassent +faluner +falunerions +faluns +famennoise +familiales +familiaux +famine +fanaisons +fanassions +fanatisait +fanatisates +fanatises +fanatisons +fanees +fanerez +fanez +fanfaronnat +fanfaronnons +fantaisie +fantasmasses +fantasment +fantasmeriez +fantasques +fantomal +fanum +faquin +farads +farandolant +farandolent +farandoliez +farcai +farcassiez +farcerai +farceront +farciez +farcirait +farcissais +farcit +fardait +fardates +fardera +farderons +fards +farfelus +farfouillas +farfouillent +farinacee +farinant +farinee +farinerent +farinez +farlouche +farouchement +fartage +fartassent +farter +farterions +fascee +fascicules +fascinai +fascinasse +fascinations +fascines +fascisames +fascisera +fasciserons +fascistes +faseillerai +faseilleront +faseyaient +faseyassions +faseyerait +faseyiez +fassies +fastigie +fat +fatalites +fatigables +fatiguames +fatigue +fatigueras +fatiguions +fatrasies +fauber +faucard +faucardas +faucardees +faucarderas +faucardeuse +fauchai +fauchards +fauchee +faucherent +fauchettes +fauchons +fauconnier +faufila +faufilasse +faufilent +faufileriez +faufilure +faunistique +faussait +faussates +fausserais +fausset +faustiennes +fautas +fauter +fauterions +fautif +fauverie +favela +favorables +favorisante +favorisates +favoriserait +favorisiez +fax +faxassent +faxer +faxerions +fayot +fayotas +fayotees +fayoterez +fayots +febricules +fecales +feciaux +fecondait +fecondassiez +feconde +feconderas +fecondions +feculames +fecule +feculera +feculeriez +feculiere +fede +federalisai +federalisera +federames +federateur +federaux +federerait +federiez +feelings +feignant +feigniez +feignit +feindrais +feint +feintassent +feinter +feinterions +feintions +felames +feldspath +felees +felerez +felibriges +felicitees +feliciterez +felide +fellaga +felle +felonnes +femelot +feminisait +feminiser +feministe +femorales +fendais +fendart +fendeuses +fendillas +fendillees +fendilleras +fendillions +fendissions +fendrait +fendue +fenestrames +fenestration +fenestrerais +fenestrez +fenetrais +fenetrat +fenetrerais +fenetrez +fennec +fenton +feodalite +ferale +ferez +feries +ferlages +ferlasses +ferlera +ferlerons +fermage +fermantes +fermaux +fermentaient +fermentasses +fermenterait +fermerai +fermeront +fermier +fermoir +feroiennes +ferraient +ferraillas +ferraillees +ferrailleras +ferrailleuse +ferrassent +ferratismes +ferrera +ferrerons +ferreuses +ferrocerium +ferronneries +ferroutait +ferroutates +ferrouterait +ferroutier +fertilement +fertilisant +fertilisat +fertilisees +fertiliserez +fertilite +fervente +fessames +fesse +fesseras +fessiere +festif +festinas +festinees +festinerez +festins +festivites +festoieras +festonnaient +festonnes +festoyait +festoyates +festoyeuses +fetames +fetassiez +feterai +feteront +feticheurs +fetichisames +fetichisez +fetidite +fetuque +feuil +feuillais +feuillards +feuillee +feuillerent +feuilleta +feuilletasse +feuilleter +feuillu +feulai +feulassiez +feulerai +feuleront +feutrage +feutrants +feutree +feutrerent +feutrines +fevier +fiabilisais +fiabilisat +fiabilisez +fiacre +fiancailles +fiancassions +fianceraient +fiances +fiasques +fibre +fibrilles +fibromateuse +fibulas +ficelaient +ficelassions +ficelier +ficellerait +ficelons +fichames +fichassions +ficheraient +fiches +fichoir +ficoides +fictive +fideismes +fidelement +fidelisasse +fidelisee +fideliserent +fidelisons +fiduciant +fieffaient +fieffassions +fiefferaient +fieffes +fiels +fientasse +fientera +fienterons +fieraient +fierions +fies +fievreux +fifties +figeait +figeates +figeraient +figes +fignolait +fignolates +fignolerait +fignoleurs +figuerie +figurai +figurasse +figurations +figurera +figurerons +figurisme +filables +filamenteuse +filandres +filao +filat +file +fileras +filet +filetas +filetees +fileterez +filets +filialement +filialisasse +filialisee +filialisons +filicophyte +filigranai +filigranerai +filipendule +fillette +filmage +filmassent +filmer +filmerions +filmions +filmologies +filochaient +filoches +filonien +filouta +filoutasse +filoutent +filouterie +filoutons +filtrai +filtrasse +filtre +filtreras +filtrions +finalisa +finalisasses +finalisent +finaliseriez +finalisons +financables +financasses +financent +financeriez +financiarisa +financiarise +financions +finassasse +finassera +finasseriez +finassier +finauderies +finesses +finiraient +finis +finissantes +finissiez +finites +finlandisa +finlandiser +finnoises +fiotes +firewire +fiscale +fiscalisas +fiscalise +fiscaliseras +fiscalisions +fissent +fissionnes +fissipede +fissurasse +fissuree +fissurerent +fissurons +fistuleuses +fitta +fittasses +fittera +fitterons +fixable +fixante +fixates +fixatrices +fixerais +fixette +fixings +fizz +flaccides +flacon +flag +flagellames +flagellates +flagellent +flagelleriez +flagellums +flageolants +flageolement +flageolerent +flageoliez +flagornas +flagornees +flagornerez +flagorneuse +flagrantes +flairant +flairee +flairerent +flaireuses +flamant +flambant +flambas +flambeaux +flamberais +flamberont +flamboiement +flamboieriez +flamboyait +flamboyiez +flaminats +flammeche +flammettes +flanant +flancha +flanchasses +flanches +flandre +flane +flanerais +flaneront +flankers +flanquantes +flanque +flanquerais +flanquez +flaques +flashant +flashat +flasherais +flasheuse +flat +flattasse +flattent +flatterie +flatulentes +flavescent +flavine +flechaient +flechassions +flecheraient +fleches +flechir +flechirions +flechites +flegmoneuse +flemmard +flemmarderai +flemmes +fletri +fletriras +fletrissait +fletrissons +fleurage +fleuras +fleurdelise +fleure +fleureras +fleuretai +fleuretons +fleuretterez +fleurie +fleuriraient +fleuris +fleurites +flexibilisa +flexibilisat +flexionnelle +flexueux +flibustais +flibustat +flibusterais +flibustez +flicailles +flinguaient +flingues +flints +flippant +flippat +flipperais +flippes +fliquait +fliquates +fliquerait +fliquesses +flirtaient +flirtassions +flirterait +flirteurs +floc +floconnaient +floconnes +flocula +floculassent +floculer +floculerions +flognarde +floquai +floquassiez +floquerai +floqueront +florales +florentines +floricoles +floridienne +florissais +flosculeuse +flottage +flottante +flottasses +flottement +flotterent +flottiez +flouait +flouates +flouerait +flouiez +floutages +floutasses +floutera +flouterons +flouzes +fluas +fluates +fluctuants +fluctuations +fluctuerent +fluctuons +fluerai +flueront +fluidifiai +fluidifiasse +fluidifiee +fluidifions +fluidisas +fluidise +fluidiseras +fluidisions +fluocompacts +fluores +fluoruration +flustre +flutasse +flutee +fluterent +flutiez +fluviatile +flux +fluxas +fluxees +fluxerez +flysurfs +focalisait +focalisates +focalises +foehn +foehnassent +foehner +foehnerions +foenes +foetopathies +foggaras +fohnas +fohnees +fohnerez +fohns +foirades +foirant +foiree +foirerent +foirez +foisonnaient +foisonner +folache +folatrait +folatrates +folatrerent +folatriez +foliaces +folichonnait +folichonne +folie +folioscope +foliotames +foliotation +folioterais +folioteur +foliques +folklorisa +folkloriser +folks +follette +folliculines +fomentas +fomentation +fomenterai +fomenteront +fon +foncames +fonce +foncerais +fonceur +fonciez +fonctionnas +fondamentale +fondas +fondation +fonder +fonderies +fondez +fondissions +fondraient +fondrons +fongibles +fongoides +font +fontanges +footballeur +footings +forain +foramines +forat +forcais +forcat +forcenees +forceras +forceur +forcions +forcirent +forcissant +forclore +fordistes +forerait +forestages +forets +forfaire +forfaitisait +forfaitisiez +forficule +forgeais +forgeat +forgerais +forges +forints +forjetas +forjetees +forjettent +forjetterons +forlancas +forlancees +forlancerez +forlane +forlignasse +forlignera +forlignerons +forlonges +formait +formalisames +formalisez +formames +formassions +formatant +formatee +formaterent +formatiez +formats +formenes +formeret +formiates +formicides +formol +formolas +formolees +formolerez +formols +formulaient +formulasses +formulent +formuleriez +formyles +forteresse +fortifiames +fortifiera +fortifierons +fortiori +fortrans +fortunella +fossane +fossiliferes +fossilisees +fossiliserez +fossoie +fossoierions +fossoyages +fossoyasses +fossoyes +fouace +fouaillait +fouaillates +fouaillerait +fouailliez +foudroie +foudroierez +foudroyaient +foudroyerent +fouet +fouettarde +fouettates +fouettes +foufous +fougeant +fougees +fougerait +fougerons +fougueuses +fouillais +fouillat +fouillerais +fouilleur +fouina +fouinards +fouinent +fouineriez +fouiniez +fouirent +fouissais +fouisseuse +foulages +foulantes +foulat +foulerais +fouleront +foulions +foulonnait +foulonnates +foulonnerait +foulonnier +foulure +fourbimes +fourbiriez +fourbissant +fourbit +fourchaient +fourches +fourchiez +fourgonnai +fourgonnerai +fourguai +fourguassiez +fourguerai +fourgueront +fourme +fourmillai +fourmillasse +fourmillent +fournaise +fournieres +fournirai +fourniront +fournisses +fournites +fourrageames +fourragee +fourrageras +fourrages +fourrames +fourre +fourrerais +fourreur +fourrions +fourvoierai +fourvoies +fourvoyerent +foutages +fouteaux +foutimassai +foutraient +foutre +foutu +fox +foyer +fracassant +fracassat +fracasserai +fracasseront +fractals +fractionnant +fractionnees +fractionnes +fractura +fracturasse +fracturee +fracturerent +fracturons +fragilisant +fragilisat +fragiliserai +fragmentai +fragmentees +fragmenterez +fragments +fraiche +fraichira +fraichirons +fraichissez +fraieraient +frairie +fraisames +fraise +fraiserais +fraisette +fraisions +framees +franchies +franchirez +franchises +franchissez +francienne +francisai +francisasse +franciscaine +francises +francistes +francophone +frangeante +frangeates +frangerait +frangiez +franglais +fransquillon +frape +frappait +frappassiez +frapper +frapperions +frappions +frasasse +frasent +fraseriez +frasons +fraternisai +fraternisez +fratries +fraudassent +fraudees +frauderez +fraudez +fraya +frayasse +frayement +frayere +frayeurs +freaks +fredonnames +fredonne +fredonnerais +fredonnez +freesias +fregatai +fregatassiez +fregaterai +fregateront +freinages +freinasses +freinera +freinerons +freinons +frelatait +frelatates +frelaterait +frelatiez +freluquets +fremirent +fremissant +fremissiez +frenatrices +freon +frequentiel +frereches +fret +fretassent +freter +freterions +fretillaient +fretiller +fretions +frettames +frette +fretteras +frettions +friabilites +fribourgeois +fricassai +fricasserai +fricasseront +friche +fricotais +fricotat +fricoterais +fricoteur +friction +frictions +frigidites +frigorifiat +frigorifiiez +frileuse +frimaires +frimassait +frimassates +frimasserait +frimassiez +frimera +frimerons +frimons +fringillides +fringuassent +fringuer +fringuerions +frioulanes +fripasse +fripent +friperie +fripieres +friponnames +friponne +friponneras +friponnez +friquee +friras +frisages +frisas +frise +friseraient +frises +frison +frisottant +frisottat +frisotterais +frisottez +frissonna +frissonnas +frissonnerez +frissons +fritait +fritates +friterait +frites +fritot +frittames +fritte +fritteras +frittions +frivolement +froidures +froissante +froissates +froisses +frolais +frolat +frolerai +froleront +frolions +fromagerie +fromentaux +fronca +froncasses +froncent +fronceriez +fronda +frondasse +frondent +fronderiez +frondiez +frontalement +fronteaux +frontistes +frottait +frottassiez +frotter +frotterions +frottions +frouames +froue +frouerez +froufroutai +froufrouter +frouillaient +frouillerait +frouilliez +frousses +fructifiai +fructifiasse +fructifieras +fructifiions +frugale +fruitages +frumentaire +frustrais +frustrasses +frustree +frustrerent +frustrons +fucales +fudge +fugace +fugua +fuguasses +fuguera +fuguerons +fuguons +fuira +fuirons +fuitai +fuitassiez +fuiterais +fuitez +fulguraient +fulgurante +fulgurates +fulgurerais +fulgurez +fuligules +fulminai +fulminasse +fulminatoire +fulminerais +fulminez +fumages +fumant +fumassent +fumer +fumeries +fumeronna +fumeronnes +fumeterres +fumier +fumigea +fumigeasses +fumigeons +fumigeriez +fumisterie +fumure +fune +funestement +funky +furculas +furetames +furete +fureterez +furetez +furfuraces +furibards +furieux +furonculose +furtivites +fusaioles +fusas +fuse +fuselaient +fuselassions +fuseliez +fusellerez +fuseologies +fuseriez +fusible +fusillades +fusillasses +fusillera +fusillerons +fusillons +fusionnames +fusionne +fusionner +fusse +fustibales +fustigeant +fustigees +fustigerent +fustine +futals +futon +future +fuyais +fuyez +gabariage +gabariassent +gabarier +gabarierions +gabarits +gabelle +gabionnage +gabionner +gables +gachai +gachassiez +gacherai +gacheront +gachions +gades +gadgetisasse +gadgetisent +gadide +gadje +gadouilles +gadrouillez +gaffait +gaffates +gafferait +gaffeurs +gagakus +gageames +gagee +gageras +gageur +gagistes +gagnaient +gagnassent +gagner +gagneries +gagnez +gaiacols +gaillarde +gailleteries +gainages +gainas +gainees +gainerez +gainiere +gaize +galactometre +galandages +galantines +galaxie +galbanums +galbee +galberent +galbons +galejaient +galejassions +galejerait +galejiez +galenistes +galerait +galerates +galererent +galeriens +galeruques +galetames +galete +galets +galetteriez +galeux +galibots +galileenne +galipette +galipotas +galipotees +galipoterez +galipots +galleuses +gallicole +gallique +gallomanie +galoches +galonnasse +galonnent +galonneriez +galonniez +galopais +galopasses +galopera +galoperons +galopine +galuchats +galvanisais +galvanisat +galvaniserai +galvauda +galvaudasse +galvaudent +galvauderiez +galvaudions +gambadais +gambadat +gambaderas +gambadeuse +gambergeai +gambergerai +gambergeront +gambiennes +gambillasse +gambillera +gambillerons +gambits +gamellames +gamelle +gamelleras +gamellions +gametocytes +gaminais +gaminat +gamineras +gaminez +gammare +gamopetale +ganaches +gangetique +gangrener +gangrenons +ganja +gansames +ganse +ganseras +gansez +gantames +gante +ganter +ganteries +gantiers +gapencaise +garagiste +garancage +garancassent +garancer +garanceries +garancez +garantie +garantirent +garantissant +garants +garbures +garconnier +gardames +garde +garderai +garderions +gardiane +gardiens +garees +gareras +gargamelles +gargarisames +gargarise +gargariseras +gargarisions +gargotant +gargotent +gargoteriez +gargotiez +gargouillons +gariez +garnie +garnirait +garnisons +garnisseur +garniture +garrocha +garrochasses +garrochera +garrocherons +garrottage +garrotter +garums +gasconnait +gasconnates +gasconnerent +gasconnisme +gaspesien +gaspillais +gaspillat +gaspillerais +gaspilleur +gasterales +gastrolatre +gastrotomies +gatames +gate +gaterais +gateront +gatifiai +gatifiassiez +gatifierais +gatifiez +gationne +gattait +gattates +gatterait +gattiez +gaucherie +gauchirai +gauchiront +gauchissait +gauchissons +gaudie +gaudirent +gaudissant +gaudriole +gaufrant +gaufree +gaufrerent +gaufrettes +gaufroir +gaulais +gaulat +gaulerai +gauleront +gaulliennes +gauloiseries +gaupe +gaussames +gausse +gausseras +gausseur +gaussions +gavait +gavates +gaverait +gaveurs +gavons +gayal +gazait +gazasse +gazeifia +gazeifiasses +gazeifient +gazeifieriez +gazeiformes +gazeras +gazetieres +gazieres +gazole +gazonnage +gazonnants +gazonnee +gazonnerait +gazonneuses +gazouillerez +gazouillez +geantes +gegene +geignants +geignez +geignissions +geindre +geishas +gelasien +gelates +gelerait +geliez +gelifiantes +gelification +gelifierais +gelifiez +gelinottes +gelivite +gelule +gemellees +gemies +geminasse +geminee +geminerent +geminons +gemiriez +gemissantes +gemissons +gemmait +gemmates +gemmerai +gemmeront +gemmions +genai +genantes +genaux +gendarmas +gendarmees +gendarmerez +gendarmette +gene +genepi +generalats +generalisait +generalisees +generasses +generee +genererent +genereux +genes +geneticien +genetismes +genevois +genial +genicules +genievres +genitalite +genitrices +genocidames +genocide +genocideras +genocideuse +genomes +genotoxique +genouillee +genre +gentianacee +gentillet +gentries +genuflexion +geocroiseurs +geodesiques +geolocalisai +geometrides +geometrisait +geometrisiez +geopelies +geopolitique +geosciences +geostrategie +gera +geraniacee +gerant +gerat +gerbant +gerbat +gerberais +gerbeur +gerbille +gercaient +gercassions +gercera +gercerons +geree +gererent +gerfaut +germa +germandree +germanisant +germanisat +germaniserai +germanite +germasse +germen +germerent +germiez +germinations +germons +gersoises +gesses +gestants +gestations +gesticulames +gesticules +gestiques +ghassoul +ghettoisas +ghettoise +ghettoiseras +ghettoisions +gibbsite +gibelotte +giboiera +giboieront +giboyant +giboyee +giboyions +giclas +giclees +gicleras +giclez +gifla +giflasses +giflera +giflerons +gigabit +gigantismes +gigogne +gigotaient +gigotassions +gigotera +gigoterons +gigotte +giguant +giguee +giguerent +gigueuses +giletieres +gindre +ginglard +gins +girafeaux +girasol +giraviation +girodyne +giron +gironnai +gironnassiez +gironnerai +gironneront +gisaient +gisements +gitai +gitas +gitees +giterez +gitologies +givrais +givrasses +givrera +givrerons +givrure +glacaient +glacassent +glacer +glaceries +glaceux +glacialement +glacier +glaciologies +glacures +glaieul +glairasse +glairent +glaireriez +glairions +glaisant +glaisee +glaiserent +glaisez +glamoureuses +glanames +gland +glandas +glandees +glanderez +glandez +glandouillez +glanduleuses +glanerai +glaneront +glanure +glapirais +glapissaient +glapissent +glaronnais +glatir +glatirions +glatisses +glaucome +glaviot +glavioter +glebes +glenais +glenat +glenerais +glenez +gley +glinglin +glissa +glissance +glissassent +glissements +glisserez +glissez +global +globalisant +globalisat +globalisees +globaliserez +globalismes +globules +gloire +gloria +glorifiaient +glorifierais +glorifiez +glosaient +glosassions +gloseraient +gloses +glossectomie +glossodynie +glotte +glougloutait +glougloute +gloussai +gloussasse +gloussent +glousseriez +glouterons +glu +gluants +gluaux +gluciniums +glucose +gluees +gluerez +gluis +glutamine +glutineux +glycerinai +glycerinerai +glyceroles +glycogenie +glycols +glyptodon +gnangnan +gneisseuses +gniole +gnome +gnose +gnosticisme +goals +gobas +gobees +gobelins +goberas +gobergeant +gobergees +gobergerent +goberiez +gobichonna +gobichonnes +gobions +godaillais +godaillat +godailleras +godaillions +godassiez +godemiches +goderas +godetias +godilla +godillasses +godilles +godillots +godronnaient +godronnes +goemon +goethite +gogeames +gogee +gogeras +gogions +goguenards +goinfraient +goinfrerons +goitreuse +golfa +golfasses +golferaient +golfes +golfons +gomarismes +gomenols +gominassent +gominer +gominerions +gommages +gommas +gommees +gommerez +gommeux +gommoses +gonadique +gonakier +gondola +gondolantes +gondole +gondolerais +gondolez +gonelle +gonfanons +gonflames +gonflassions +gonflera +gonflerons +gonflons +goniometrie +gonocytes +gonze +gord +goret +gorgeames +gorgee +gorgerait +gorgerins +gorgonaire +goron +gosplans +gossassent +gosser +gosserions +gothas +gouacha +gouachasses +gouachera +gouacherons +gouaillais +gouaillat +gouaillerais +gouailleront +goualante +goudronnage +goudronner +goudronneux +gouffre +gougeas +gougent +gougerent +gougions +goujats +goujonnasse +goujonnent +goujonneriez +goujonniez +goulafre +goulees +goulot +goumier +goupillant +goupillee +goupillerent +goupillon +gourami +gourasses +gourbivilles +gouren +gourerent +gourgane +gourmandais +gourmandat +gourmandez +gourmantches +gournablai +gournablerai +gourounsi +gout +goutassent +gouter +gouterions +goutez +gouttames +goutte +goutteras +goutteur +gouttons +gouvernail +gouvernants +gouvernee +gouvernerai +gouverneront +gouvernions +goyesques +graciable +graciassent +gracier +gracierions +graciez +grada +gradasses +gradee +graderent +gradients +graduaient +graduant +graduateurs +graduelles +graduerent +graduons +graffitais +graffitat +graffiterais +graffiteur +graffs +grafigner +graillaient +graillera +graillerons +graillonnais +graillons +grainant +grainee +grainerent +grainetiere +grainieres +graissaient +graisses +gram +grammages +grand +grandet +grandirai +grandiront +grandit +granitaient +granites +granitiques +grannys +granulant +granulations +granulerais +granuleuse +granum +grapheur +graphismes +graphitames +graphite +graphiteras +graphiteux +graphologies +graphometres +grappillai +grappillerai +grappillons +grasses +grasseyames +grasseyera +grasseyerons +grassouillet +graticulera +gratifiais +gratifiasses +gratifient +gratifieriez +gratina +gratinasses +gratinera +gratinerons +gratis +gratouillait +gratouille +gratouillons +grattas +grattees +gratterais +grattes +grattoirs +grattouillas +grattouiller +gratuit +gravaient +gravassions +gravelage +gravelassent +gravelerent +gravelle +gravent +graveriez +graveurs +gravidites +gravillon +gravillonnas +gravillonnes +gravimetres +gravirais +gravisphere +gravissiez +gravitaires +gravitassent +gravite +graviterez +gravitons +grazioso +greant +grebes +grecisait +grecisates +greciserait +grecisiez +grecquais +grecquat +grecquerais +grecquez +gredins +greera +greerons +greffaient +greffassions +grefferaient +greffes +greffiez +gregarisme +greiez +grelasse +grelent +greleriez +grelez +grelottai +grelottasse +grelottent +grelotteriez +greluches +grenache +grenadant +grenadee +grenaderent +grenadien +grenadilles +grenaient +grenaillas +grenaillees +grenaillerez +grenais +grenassiez +grenelaient +greneliez +grenellerez +grenerai +greneront +greniez +grenouillat +grenouillere +grenus +gresames +grese +greseras +greseuse +gresillaient +gresilles +gresons +grevant +grevee +greverent +grevilleas +gribouillage +gribouillera +gribouillons +griffages +griffasses +griffera +grifferons +griffoir +griffonnames +griffonne +griffonneur +grifftons +grignai +grignas +grigner +grignerions +grignotage +grignoterez +grignotez +grigris +grillage +grillager +grillaient +grillasses +grillera +grillerons +grills +grimacante +grimacates +grimacerait +grimacier +grimaient +grimassions +grimera +grimerons +grimpai +grimpasse +grimpent +grimperent +grimpeur +grimpons +grincants +grincement +grincerent +grinchaient +grincherait +grincheuses +gringalets +griotte +grippai +grippasse +grippees +gripperas +grippions +grisaillais +grisaillat +grisaillez +grisants +grisates +griserie +grisettes +grisollant +grisollement +grisollerent +grisollons +grisonnantes +grisonne +grisonneras +grisonnions +grisouteuses +grivelas +grivelees +grivelez +grivelleras +grives +grivoiseries +grognard +grognassames +grognassent +grogne +grognerais +grogneront +grognonna +grognonnes +groins +grommelais +grommelat +grommelions +grommelleras +grondaient +grondassent +grondements +gronderez +grondeuse +groove +grosserie +grossiere +grossirai +grossiront +grossit +grossoierez +grossoyait +grossoyates +grossoyons +grouillait +grouiller +group +groupales +groupates +grouperai +grouperont +groupions +growlers +grueries +grugeas +grugent +grugeras +grugions +grumelai +grumelassiez +grumeleuse +grumelons +gruppetti +grutames +grute +gruteras +grutiere +gryphee +guanaco +guarani +guea +gueasse +guedes +gueerait +gueguerre +guelte +guenons +guerba +guerezas +guerimes +gueririez +guerissais +guerissez +guerre +guerroya +guerroyasses +guerroyes +guesdisme +guetrai +guetrassiez +guetrerai +guetreront +guettaient +guettassions +guetteraient +guettes +gueulai +gueulardes +gueule +gueulerais +gueuleton +gueusaient +gueusasses +gueusera +gueuseriez +gueuze +guibole +guichetier +guidais +guidassiez +guider +guiderions +guidons +guignames +guignassiez +guignerai +guigneront +guignolade +guildes +guillemetee +guillemette +guillochages +guillochera +guillochis +guillotinait +guillotine +guimbardes +guimpassent +guimper +guimperions +guinchaient +guincherait +guinchiez +guindaillat +guindas +guindeaux +guinderas +guindez +guingois +guipames +guipe +guiperas +guipions +guisarme +guitounes +gummiferes +gunitas +gunitees +guniterez +gunitions +guru +gustation +guttifere +guyanienne +gymkhanas +gymnasienne +gymnocarpes +gynandrie +gypseries +gypsophiles +gyrolasers +gyropilotes +gyrotrain +habilete +habilitasse +habilitee +habiliterent +habilitons +habillames +habille +habillerais +habilleur +habitabilite +habitames +habitassions +habiter +habiterions +habituai +habituassiez +habituees +habitues +hablait +hablates +hablerent +hableurs +hacha +hachasse +hachement +hacherait +haches +hachis +hachurait +hachurates +hachurerait +hachuriez +hackeuses +haddocks +hadji +hafsides +haguenoviens +haie +hainteny +hairais +hairont +haisses +haitiens +halai +halant +halbis +halee +halenant +halenee +halenerent +halenons +halerez +haletait +haletassiez +haleterai +haleteront +haleuse +haligonien +halite +hallalis +halloweens +hallucinait +hallucinees +hallucinerez +halogenait +halogenates +halogenes +haloirs +halophytes +hamadryas +hameconnes +hammam +hanafisme +hanbalite +hanchasse +hanchement +hancherent +hanchons +handicapai +handicapasse +handicapent +handicapons +hannetonna +hannetonner +hanoiennes +hansarts +hantais +hantat +hanteraient +hantes +hapalides +haplonte +happant +happee +happeraient +happes +haptonomies +harakiris +haranguas +haranguees +haranguerez +haranguez +harassaient +harassassent +harassements +harasserez +harcela +harcelas +harcelees +harceleras +harceleuse +harcellerais +hard +hardassent +hardees +harderez +hardez +hardons +harengeres +harfangs +haridelles +harkis +harmonisa +harmonisent +harmonistes +harnachas +harnachees +harnacheras +harnachez +harpai +harpas +harpees +harperez +harpions +harponnaient +harponnera +harponnerons +harraga +haruspices +hasardasse +hasardent +hasarderiez +hasardiez +haschischin +hasseltoises +hast +hatai +hatassiez +hatelettes +hateras +hatez +hativement +haubanages +haubanasses +haubanera +haubanerons +haubert +haussasse +haussement +hausserent +haussieres +hautain +hauterivien +hautins +havages +havanes +have +haverai +haveront +haviez +havirent +havissant +havons +hawaien +hazan +heaumiers +hebelomes +hebergeaient +hebergera +hebergerons +hebertisme +hebetant +hebetee +hebeterait +hebetiez +hebraisai +hebraisasse +hebraisent +hebraiseriez +hebraisons +hectiques +hederacees +hedychiums +hegemoniques +hein +helassent +helepole +helerez +helianthemum +helicases +helicoide +heligare +heliodores +helions +heliportames +heliporte +heliporteras +heliportions +helitreuilla +helladiques +hellenisais +hellenisent +hellenisons +helminthique +heloderme +helvelles +helvetismes +hemathidrose +hematite +hematologie +hematophage +hematuries +hemialgies +hemicycles +heminee +hemitropes +hemocultures +hemogramme +hemopathie +hemoptysique +hemostatique +hennins +henniriez +hennissantes +hennissons +heparine +hepatite +hepatologue +heptaedres +heptametres +heraultaises +herbageai +herbager +herbagerez +herbagez +herbasse +herbent +herberie +herbeuses +herbions +herborisant +herborisera +herboristes +herchais +herchat +hercheras +hercheuse +herculeens +heredite +heresiarques +herissais +herissat +herisserai +herisseront +herissonnait +herissonne +herissonnons +heritaient +heritassions +heriteraient +herites +hermandads +hermetique +herminettes +herniee +heroiquement +heroisasse +heroisee +heroiserent +heroisme +herons +hersait +hersates +herschas +herscher +herscherions +herschions +herserais +herseur +hertzienne +hesitai +hesitasse +hesitent +hesiteriez +hessoise +heterie +heteroclite +heterodoxe +heterogamies +heterotherme +hettangien +heure +heurta +heurtasses +heurtera +heurterons +hevea +hexadecimal +hexagonal +hexamidine +hexastyle +hiais +hiat +hibernais +hibernas +hibernaux +hibernerent +hiberniez +hidalgos +hiemations +hierait +hieras +hierodule +hierons +hifi +hiions +hilares +hiloires +himalayismes +hindis +hindoustanie +hipparions +hippologies +hippophages +hircine +hirsutismes +hispanisai +hispanisasse +hispanisee +hispanisme +hispides +hissas +hissees +hisserez +histamine +histochimie +histologies +historiai +historie +historierai +historieront +historisme +hittiste +hivernais +hivernas +hivernee +hivernerait +hiverniez +hobbys +hochais +hochat +hochequeue +hocherez +hochions +hodographe +hola +hollandaise +holocauste +holographes +holomorphes +holosteen +holsteins +homards +homeostat +homerique +hominides +hominises +homo +homogames +homogeneisa +homogeneisat +homogeneisez +homographies +homologables +homologuais +homologuat +homologuez +homoncule +homophobies +homotherme +homozygotie +hongkongais +hongras +hongrees +hongrerez +hongrions +hongroierie +hongrons +hongroyas +hongroyees +honing +honnie +honnirent +honnissant +honora +honorais +honorassiez +honorerai +honoreront +honteuse +hopaks +hoqueta +hoquetasses +hoquetions +hoquetterait +horaires +horeca +horloges +hormonames +hormonaux +hormonerait +hormoniez +horodataient +horodates +horographies +horreurs +horrifiant +horrifiat +horrifierais +horrifiez +horripilait +horripilees +horripilerez +hors +hortensias +hospitaliere +hospitalisas +hospitaliser +hospodar +hosties +hotdog +hotels +hottais +hottat +hotter +hotteret +hotteuse +houache +houames +houat +houblonnames +houblonne +houblonneras +houblonniere +houee +houerent +houiller +houkas +houliganisme +houppa +houppasses +houppent +houpperiez +houppiez +hourdaient +hourdassions +hourderaient +hourdes +hourques +houseau +houspillas +houspillees +houspillerez +houspillez +houssaies +houssassions +housseraient +housses +houssinames +houssine +houssineras +houssinions +hovercrafts +huais +huassent +hublots +huchas +huchees +hucherez +huchiers +huerai +hueront +huguenote +huilaient +huilassions +huileraient +huilerons +huiliers +huissieres +huitantieme +huitriers +hululait +hululates +hululerait +hululiez +humaient +humanisai +humaniser +humaniste +humanoide +humates +humectais +humectat +humecterai +humecteront +hument +humeras +humeur +humidifient +humidifuges +humilia +humilias +humilie +humilieras +humiliions +humorales +humus +hunters +hurdleuse +hurlant +hurlat +hurlerai +hurleront +hurluberlu +huronnes +hussarde +hutoise +hyades +hyaloides +hybridames +hybridation +hybriderais +hybrideur +hybridites +hydatique +hydrangeas +hydrargies +hydrataient +hydratassent +hydratees +hydraterez +hydraule +hydravion +hydrique +hydrocutasse +hydrocutent +hydrofugea +hydrofugerai +hydrogenais +hydrogenat +hydrogenerai +hydrohemies +hydrologies +hydrolysai +hydrolysera +hydronymies +hydrophobes +hydropiques +hydrospeeds +hydrozoaire +hygiene +hygrometres +hygrophobes +hygrostats +hylozoismes +hymens +hyoidiens +hyperactifs +hyperalgies +hyperbates +hyperboreens +hyperemies +hyperlien +hyperonymies +hyperplans +hypersonique +hypertendu +hypertensive +hyphes +hypnoides +hypnotisant +hypnotisee +hypoalgesies +hypocapnies +hypocondre +hypocras +hypocycloide +hypogastre +hypoides +hypolipemie +hypomanies +hyponymes +hypophyses +hyposodee +hypostasiait +hypostasie +hypostasions +hypotensif +hypothalamus +hypothequais +hypoxique +hysope +hysterisas +hysterisees +hysteriserez +iakoute +ibere +iberis +icaques +iceberg +ichthyose +ichtyosaure +icones +iconolatres +iconologues +icosaedraux +ide +idealisames +idealisateur +idealiser +idealiste +ideelle +identifiable +identifiasse +identifies +identitaire +ideologie +ideomoteurs +idiomatique +idiotifia +idiotifiera +idoine +idolatrasse +idolatrent +idolatreriez +idolatrique +idylliques +igbos +ignames +igniferes +ignifugeant +ignifugeat +ignifugerais +ignifugez +ignitions +ignominie +ignorames +ignorantiste +ignorates +ignorerait +ignoriez +igues +ile +ileocaecaux +ileus +illegale +illicitement +illiquidites +illocutoires +illuminait +illuminates +illumines +illusionna +illusionnes +illusoire +illustras +illustratif +illustrees +illustrerez +illutasse +illutee +illuterent +illutons +illuviums +ilombas +ilotisme +imageames +imagee +imageras +imageur +imagina +imaginale +imaginat +imaginee +imaginerent +imaginons +imam +imbecile +imbibai +imbibassiez +imbiberai +imbiberont +imbittable +imbriquant +imbriquee +imbriquerent +imbriquons +imbue +imines +imitames +imitateur +imite +imiteras +imitions +immanent +immanquable +immatricule +immaturation +immediatete +immensement +immergeais +immergeat +immergerais +immergez +immersions +immigrais +immigrasses +immigrent +immigreriez +imminences +immiscames +immisce +immisceras +immiscions +immobilisa +immobiliser +immobilistes +immodeste +immolames +immolateur +immoler +immolerions +immondice +immoraliste +immortel +immuable +immunisait +immuniser +immunite +immunogene +impact +impactants +impactee +impacterent +impactions +impalas +impanations +imparidigite +impartie +impartirent +impartissant +impartition +impassibles +impatientait +impatientez +impatronisai +impayees +impecs +impedimenta +impensable +imperfectif +imperforees +imperiaux +imperiums +impetrai +impetrasse +impetree +impetrerent +impetrons +impietes +implacables +implantant +implanterait +implantiez +implementait +implementiez +impliqua +impliquas +impliquees +impliquerez +implora +imploras +implore +imploreras +implorions +implosasse +implosera +imploserons +implosives +impolis +impopularite +importames +importasses +importerait +importiez +importunames +importune +importuniez +imposaient +imposassent +imposer +imposerions +imposions +imposteur +impotents +impregna +impregnasse +impregnee +impregnerent +impregnons +impressifs +imprimante +imprimates +imprimerais +imprimeront +impro +impromptue +impros +improuvas +improuvees +improuverez +improvisa +improvisiez +imprudents +impudent +impudiques +impulsais +impulsat +impulserais +impulsez +impulsive +impunis +impuretes +imputait +imputates +imputeraient +imputes +inabouti +inaccentue +inacceptees +inachevement +inactivai +inactiver +inactualite +inadapte +inalienes +inalpames +inalpe +inalperas +inalpions +inamendable +inanites +inapercue +inappreciee +inapproprie +inarretable +inattendus +inaugurai +inaugurasse +inaugurerai +inaugureront +inavouable +incapacites +incarceras +incarcere +incarcereras +incarcerions +incarnait +incarnate +incarnera +incarnerons +incasable +incendiaires +incendies +incertaines +inchange +inchavirable +incidemment +incinerai +incinerees +incinererez +incipit +incisait +incisates +inciserait +incisiez +incisures +incitants +incitateurs +incitee +inciterent +incitons +incivique +inclementes +inclinait +inclinassiez +incliner +inclinerions +incluaient +incluions +inclurent +inclusifs +inclussions +incognitos +incollables +incombent +incomburants +incommodant +incommodat +incommodes +incommutable +incompetente +inconduites +incongrue +inconsciente +inconsidere +inconstances +inconteste +inconvenant +incorporeite +incorpores +incrementa +incrementer +incrementons +incriminait +incriminates +incrimines +incroyance +incrustait +incruster +incrustions +incubasse +incubations +incuberaient +incubes +inculcations +inculpas +inculpe +inculperas +inculpions +inculquasse +inculquent +inculqueriez +incultes +incunables +incurieux +incurvames +incurvation +incurverais +incurvez +indaguais +indaguat +indagueras +indaguions +indecises +indefini +indelebile +indemnes +indemnisas +indemnise +indemniseras +indemnisions +indenouables +indentassent +indentees +indenterez +indepassable +independants +indetermines +indexa +indexant +indexations +indexerait +indexeurs +indianisais +indianisat +indianiserai +indianologie +indicans +indicateur +indice +indiceras +indiciaires +indicons +indiffera +indifferents +indigenats +indigente +indigetes +indignassent +indignees +indignerent +indignite +indigotines +indiquassent +indiquer +indiquerions +indiscret +indiscutee +indisposes +indissoluble +indivis +indivisibles +indol +indolents +indomptee +indou +indriens +inductible +inductrice +induire +induisant +induisisse +induites +indulgencier +induline +indurames +induration +indurerais +indurez +indusies +industrieux +inecoutables +ineducables +inefficacite +inegalee +inelastiques +ineligible +inemplois +inentamables +ineprouves +inepuisees +inerta +inertasse +inertent +inerteriez +inertiels +inesperes +inetendue +inexactitude +inexcitables +inexecution +inexigible +inexorable +inexperte +inexploites +inexpressifs +infames +infantile +infants +infarcirait +infarcissais +infarcit +infatuaient +infatuera +infatuerons +infecondite +infectant +infectat +infecterais +infectez +infections +infeodai +infeodassiez +infeoder +infeoderions +inferaient +inferassions +infererent +infernal +infertiles +infestas +infeste +infesteras +infestions +infibulant +infibulerait +infibuliez +infidelite +infiltrasse +infiltre +infiltreras +infiltrions +infinites +infinitudes +infirmassent +infirmative +infirmerais +infirmeront +infirmites +inflammation +inflechi +inflechiras +inflexions +infligeasse +infligeons +infligeriez +influa +influasses +influencable +influencer +influentes +influeras +influions +infographie +informa +informasses +informatif +informatisat +informel +informerait +informiez +inforoutes +infoutues +infradiennes +infrasons +infructueuse +infusa +infusasses +infusera +infuserons +infusiez +ingelif +ingeniant +ingeniee +ingenierent +ingenies +ingeniiez +ingenument +ingerant +ingeree +ingererait +ingeriez +ingrate +ingression +inguinales +ingurgitasse +ingurgitee +ingurgitons +inhabite +inhabituels +inhalant +inhalateurs +inhalera +inhalerons +inharmonies +inherentes +inhibantes +inhibe +inhiberas +inhibions +inhibitoires +inhumais +inhumassiez +inhumer +inhumerions +inimitable +inique +initial +initialent +initialeriez +initialisat +initialises +initiassent +initiatique +initient +initieriez +injectable +injectassent +injecter +injecterions +injection +injonctifs +injuriai +injuriassiez +injurierai +injurieront +injuste +injustifies +innee +innervaient +innervassent +innervees +innerverez +innes +innocentames +innocente +innocenteras +innocentions +innomes +innommes +innovants +innovateurs +innovera +innoverons +innus +inobserves +inoculable +inoculassent +inoculatrice +inoculerais +inoculez +inoffensif +inondait +inondates +inonderaient +inondes +inoperants +inoubliables +inquietai +inquietasse +inquietent +inquieteriez +inquietude +inquisitif +insalubrite +insatiable +inscription +inscririez +inscrivais +inscriviez +inscrivit +insculpas +insculpees +insculperez +insecabilite +insectifuge +insecurisant +inseminaient +inseminee +inseminerent +inseminons +inserant +inseree +insererent +insermente +insidieux +insinuames +insinuera +insinuerons +insipidites +insistante +insistates +insisterent +insistons +insolai +insolassiez +insolees +insolerai +insoleront +insomniaque +insonores +insonorisent +insortable +insoucieux +insoupconne +inspectait +inspectates +inspecterait +inspecteurs +inspira +inspiras +inspiration +inspirer +inspirerions +instable +installasse +installes +instantanee +instaura +instaurasses +instaurerait +instauriez +instigua +instiguasses +instiguera +instiguerons +instillais +instillat +instiller +instinctifs +instinctuels +instituant +instituee +instituerent +instituons +instructrice +instruiriez +instruisent +instrument +insuffisants +insufflerais +insufflez +insularites +insulta +insultas +insultees +insulterez +insultez +insupportait +insupporte +insupportons +insurgeasse +insurgeons +insurgeriez +intacts +intailler +intangible +integraient +integrant +integrassiez +integrative +integrer +integrerions +integristes +intellocrate +intemperies +intemporels +intensement +intensifias +intensifiees +intension +intensives +intentassent +intenter +intenterions +interactifs +interagirais +interallies +intercalaire +intercaler +intercedasse +intercedera +interceptat +intercurrent +interdiction +interdirais +interdis +interdisions +interdits +interessants +interessee +interessiez +interfacai +interfacerai +interfecond +interfera +interfereras +interferions +interfoliage +interfoliera +interieures +interjetai +interjetez +interlignage +interlignera +interlope +interloquer +intermezzos +internaient +internalisas +internaliser +internassent +internats +internent +interneriez +interniste +interosseuse +interpelais +interpelat +interpelions +interpelles +interpolez +interposant +interposee +interpositif +interpretais +interpretera +interrogatif +interroge +interrogeas +interrogeons +interrompra +intersectes +intersexuel +intersignes +interurbaine +intervenant +interviewee +interzonales +intestinaux +intimais +intimat +intimera +intimerons +intimidait +intimide +intimideras +intimidions +intimons +intitulaient +intitules +intolerant +intonative +intoxicante +intoxiquames +intoxique +intoxiqueras +intoxiquions +intraitable +intrepidites +intriguais +intriguat +intriguerais +intriguez +intriquais +intriquat +intriquerais +intriquez +introduiras +introduisait +introduisis +introduites +intronisai +introniser +introrses +introspectez +introuvables +intrusif +intubait +intubates +intuberaient +intubes +intuitames +intuite +intuiteras +intuitif +intuitives +inuite +inuktituts +inusites +invaginaient +invaginera +invaginerons +invaincus +invalidants +invaliderait +invalidiez +invariables +invasion +invectivant +invectivee +invectivons +invenges +inventas +inventees +inventerez +inventif +inventoriage +inventoriera +inverifie +inversait +inversates +inverserais +inverseur +inversive +invertie +invertirait +invertissais +invertit +investiguera +investis +investisseur +investiture +inveterasse +inveterent +invetereriez +inviolee +invitaient +invitassent +invite +inviteras +inviteuse +invocation +involucre +involution +invoquant +invoquee +invoquerent +invoquons +iodait +iodate +ioderais +iodeuse +iodisme +iodlasse +iodlera +iodlerons +iodlons +ionienne +ionisames +ionisassions +ionisera +ioniserons +ionisons +ioulames +ioule +ioulerez +iourte +ipomee +iqalummiuq +irakiens +iraquienne +ireniques +iridees +iridiennes +iridologues +irisables +irisasses +irisent +iriseriez +irlandais +ironiquement +ironisasse +ironisera +ironiserons +iront +irrachetable +irradiant +irradiat +irradier +irradierions +irraisonne +irrationnel +irrealisees +irrecevable +irreflechi +irrefrenable +irrefutes +irreligieux +irresolution +irrigation +irriguant +irriguee +irriguerent +irriguons +irritames +irritassions +irritee +irriterent +irritons +isards +ischemique +iseranes +islamisa +islamisas +islamise +islamiseras +islamisions +islandaise +ismailien +isobathes +isoceles +isocliniques +isoedriques +isogamies +isogones +isoioniques +isolante +isolates +isolatrices +isolerai +isoleront +isoloirs +isomerisa +isomerisent +isometries +isoniazides +isophases +isorels +isostatiques +isotopies +israelienne +issas +istreenne +italianisant +italianisez +italienne +iterais +iterat +iteree +itererent +iterons +itinerantes +ivoirerie +ivoiriers +ivres +ivrognesses +ixames +ixe +ixeras +ixias +jabirus +jablas +jablees +jablerez +jablieres +jabot +jabotas +jaboter +jaboterions +jabotions +jacaranda +jacassasse +jacassent +jacasserie +jacasseuses +jacees +jacistes +jacobee +jacobinismes +jacquarde +jacqueries +jactaient +jactasses +jactera +jacterons +jactons +jaguar +jaillirai +jailliront +jain +jakartanaise +jalonnais +jalonnat +jalonnerai +jalonneront +jalons +jalousassent +jalousent +jalouseriez +jalousons +jamaiquains +jambees +jambonneaux +jamerose +janotisme +janthines +japonais +japonisais +japonisasses +japonisera +japoniserons +japonistes +jappasse +jappent +japperiez +jappiez +jaques +jardes +jardinant +jardinee +jardinerent +jardinets +jardinions +jargonnerai +jargonneront +jargonniez +jarousses +jarretait +jarretates +jarretiere +jars +jartassent +jarter +jarterions +jasai +jasasse +jasement +jaserans +jasette +jasions +jaspaient +jaspassions +jasperaient +jaspes +jaspinas +jaspinees +jaspinerez +jaspions +jassames +jasse +jasserez +jassez +jattees +jaugeames +jaugee +jaugeras +jaugeuse +jaunatres +jaunimes +jauniriez +jaunissant +jaunissiez +javarts +javelai +javelassiez +javeleur +javellent +javellerons +javellisames +javellisera +javotte +jazzmans +jeannotisme +jejunale +jennys +jerkames +jerke +jerkerez +jerks +jerricans +jesuite +jetable +jetas +jetees +jetions +jette +jetterions +jeunaient +jeunassions +jeunerais +jeunesse +jeunez +jeunotte +jihadiste +jivaros +jobard +jobardassent +jobarder +jobarderies +jobardises +jocasse +jocrisse +jodlait +jodlates +jodlerent +jodleuses +joggames +jogge +joggerez +joggeuses +johannique +joignabilite +joignes +joignissiez +joindras +jointage +jointassent +jointements +jointerez +jointions +jointons +jointoyerent +joints +jokers +joliettaines +jonagold +joncames +jonce +jonceras +jonchai +jonchassent +jonchements +joncheras +jonchet +jonction +jonglant +jonglent +jonglerie +jongleuses +jonquilles +josephisme +jouabilite +jouaillait +jouaillates +jouaillerent +jouaillons +jouasse +joue +joueras +jouet +joufflue +jouira +jouirons +jouissantes +jouissiez +joujoutheque +journaleuse +journalisais +journees +joutas +jouter +jouterions +joutions +jouxtaient +jouxtassions +jouxteraient +jouxtes +jovialites +joyeusement +jubes +jubilante +jubilates +jubileraient +jubiles +juchames +juche +jucheras +juchions +judaisaient +judaisassent +judaisees +judaiserez +judaismes +judeens +judicatures +judicieux +jugale +jugeait +jugeates +jugera +jugerons +juglandacee +jugulait +jugulates +jugulerait +juguliez +juins +jujubiers +juliens +jumel +jumelas +jumelees +jumellerai +jumelles +jumpa +jumpasses +jumperaient +jumpers +juncos +junker +junoniens +jupiers +juponnaient +juponnes +juraient +jurasse +jurassiques +jurements +jurerez +jurez +juriez +jurons +jusquiame +jussion +justice +justifia +justifiantes +justifie +justifieras +justifiions +jutasse +jutera +juterons +juvenat +juxtaposera +kabbaliste +kabouli +kabye +kaddish +kagou +kakawi +kale +kalis +kamalas +kampalaise +kanangais +kanglar +kans +kaolackoise +kaolinisais +kaolinisat +kaoliniserai +kaons +karakul +karbaux +karman +karstique +kasaiennes +katakana +kathak +kawas +kayaks +kebabs +keftas +kemalistes +kens +kenyane +kepis +keratinisa +keratiniser +keratiques +kern +ket +keum +keynesiennes +khalifes +khanats +khedives +khmeres +khols +kibitzait +kibitzates +kibitzerait +kibitziez +kicks +kidnappant +kidnappee +kidnapperent +kidnappeuses +kieselguhr +kif +kifassent +kifer +kiferions +kiffames +kiffe +kifferas +kiffions +kiki +kilobase +kiloeuro +kilohms +kilometrames +kilometre +kilometreras +kilometrions +kilotonnes +kilowatts +kimmeridgien +kine +kinkajou +kiosques +kipper +kiribatienne +kit +kiwi +klaxonnas +klaxonnees +klaxonnerez +klaxons +klezmer +knesset +koalas +kodaks +kois +kolkhoze +komsomol +konzern +koras +korrigane +kotai +kotassiez +koterais +koteur +kots +kouffas +koupreys +koweitiens +kraken +kremlins +krill +krypton +kufique +kundalini +kwacha +kymographes +kystique +labarum +labelisais +labelisat +labeliserai +labeliseront +labellisera +labferment +labialisais +labialisat +labialiserai +labies +laboratoire +labourables +labourasse +labourent +laboureriez +labourons +labre +labyrinthe +lacais +lacassent +laccolite +lacees +lacerames +laceration +lacererais +lacerez +lacertiens +lacez +lachas +lachees +lacherent +lacheur +laciniee +laconiens +lacrymale +lactalbumine +lactation +lactescentes +lactoducs +lactoserum +lacuneuses +ladanums +ladite +laetare +lagopede +lagunage +laicard +laicisaient +laicisera +laiciserons +laicistes +laiderons +laierais +laimargues +lainas +lainees +lainerez +laineurs +lainions +laissais +laissat +laisserais +laissez +laitee +laitier +laitonnais +laitonnat +laitonnerais +laitonnez +laiussai +laiussassiez +laiusserais +laiusseur +lakh +lamages +lamait +lamarckienne +lamaserie +lamba +lambeau +lambina +lambinasse +lambinera +lambinerons +lamblias +lambrissages +lambrissera +lambruscos +lamellaires +lament +lamentant +lamentations +lamenterait +lamentiez +lamerait +lamiacee +lamifiee +laminais +laminat +laminerai +laminerions +laminiez +lampadophore +lampants +lampassions +lampera +lamperon +lampiste +lamproies +lanaudoise +lancant +lancee +lancera +lancerons +lancez +lancinant +lancinat +lanciner +lancinerions +lancons +landaus +landiers +landsturm +langagier +langeames +langee +langeras +langhienne +langoustes +langueya +langueyasse +langueyent +langueyeriez +languide +languiraient +languis +languissent +lanice +laniide +lantanas +lanternas +lanterneaux +lanterneras +lanternez +lanthanides +laobes +laotiens +lapames +lapassent +lapements +lapereaux +lapicide +lapidant +lapidations +lapiderait +lapideurs +lapidifiames +lapidifiez +lapillis +lapinasse +lapinera +lapinerons +lapinons +lapones +lapsus +laquames +laque +laquerait +laqueurs +larbin +lardage +lardassent +larder +larderions +lardonna +lardonnasses +lardonnera +lardonnerons +lares +larget +largua +larguasses +larguera +larguerons +laride +larmier +larmoierait +larmoyaient +larmoyassent +larmoyeurs +larronnesse +larves +laryngologie +laryngoscope +lascar +lascivites +lassante +lassates +lasserait +lasses +lassos +lasurait +lasurates +lasurerait +lasuriez +latents +lateralises +lateritiques +latif +latinisaient +latinisees +latiniserez +latinismes +latitudes +latrines +lattait +lattates +latterait +lattiez +laudateur +laudienne +lauree +lauriers +lavabilites +lavages +lavames +lavante +lavassions +lavees +laveras +lavette +lavions +lavures +laxismes +layais +layat +layerais +layetier +layons +lazurites +leaderships +lecanores +lechas +lechees +lecherais +lecheront +lechouilla +lechouilles +lecons +lectisternes +ledit +legalisa +legalisasses +legalisent +legaliseriez +legalisons +legato +legendais +legendat +legenderais +legendez +legers +legiferait +legiferates +legifererent +legiferons +legionnaire +legitimaient +legitimasses +legitimement +legitimerez +legitimismes +leguai +leguassiez +leguerai +legueront +legumiers +leibnizien +leipzigoises +lek +lemmatisames +lemmatisez +lemniscate +lemurien +lenifiaient +lenifiassent +lenifier +lenifierions +leniniste +lente +lenticulee +lentilles +lents +leone +leontodon +leotard +lepisme +lepreux +leprome +leptocephale +leptosome +leptynite +lesaient +lesassions +lesbismes +leseraient +leses +lesinas +lesiner +lesineries +lesinez +lesionnais +lesionnat +lesionner +lesothan +lessivages +lessivasses +lessivera +lessiverons +lessivielles +lestages +lestasses +lester +lesterions +letal +lethargique +lettones +lettrait +lettrates +lettrerait +lettreurs +lettrismes +leucemiques +leucoma +leucopoiese +leude +leurras +leurrees +leurrerez +leurs +levais +levantin +levassions +leveraient +levers +levigation +levigeas +levigent +levigerez +levirat +levitai +levitassiez +leviterai +leviteront +levons +levrettais +levrettat +levretterais +levrettez +levs +levurames +levure +levureras +levuriers +lexicalisai +lexicalisera +lexique +lezardames +lezarde +lezarderas +lezardions +liaison +liaisonner +liames +liants +liardasse +liardera +liarderons +liasiques +libanais +libellai +libellassiez +libellerai +libelleront +liber +liberalement +liberalisees +liberalisons +liberassions +liberatrices +libererais +liberez +liberistes +liberte +liberty +libidos +librement +libyen +licences +licencias +licenciees +licencieras +licencieuses +lichais +lichat +lichent +licheriez +licheuses +licitai +licitassiez +licitees +liciterent +licitons +lidars +liees +liegeasse +liegeois +liegeras +liegeuse +lient +lieriez +liesses +lieuses +liez +liftasse +liftent +lifteriez +liftier +ligament +ligases +ligaturer +ligerienne +lignagere +lignard +ligne +ligneras +lignette +lignicole +lignifias +lignifie +lignifieras +lignifiions +lignometres +ligotames +ligote +ligoteras +ligotions +liguas +liguees +liguerez +liguez +ligure +lilas +limace +limais +limassent +limbe +limee +limera +limeriez +limeurs +liminaires +limitaient +limitassent +limitative +limiteraient +limites +limivores +limniques +limogeaient +limogeraient +limoges +limonadieres +limonames +limone +limonerais +limoneuse +limonite +limousin +limousinas +limousinees +limousinerez +limousins +linaigrette +lindor +liners +linger +lingota +lingotasses +lingotera +lingoterons +lingotons +linguet +linguistique +linkage +linoleine +linottes +linsoir +lionnes +lipidemie +lipochromes +lipoidique +lipophile +liposoluble +liposucant +liposuccions +liposucerait +liposuciez +lippee +liquefiai +liquefiasse +liquefient +liquefieriez +liquettes +liquidambar +liquidat +liquidatrice +liquiderais +liquidez +liquoreuse +liras +liront +lisbonnais +liserage +liserassent +liserer +lisererions +lisette +lisiblement +lisps +lissante +lissates +lisserait +lisseurs +lissoir +listames +liste +listerai +listerions +listions +litaient +litasse +liteau +literait +lites +lithiasique +lithium +lithogenes +lithophanies +lithotriteur +litiez +litions +litrages +litrasses +litrera +litrerons +litsams +litterarite +littorale +liturgies +livarot +livides +livrables +livrasse +livrent +livreriez +livreurs +lixiviais +lixiviat +lixiviera +lixivierons +llanos +lobaient +lobasses +lobbyismes +lobectomies +lober +loberions +lobis +lobotomisant +lobotomisees +lobulaire +locale +localisais +localisat +localisees +localiserez +localismes +locatif +locavores +lochasse +lochent +locheriez +lochs +lockoutames +lockoute +lockouteras +lockoutions +locomotion +locos +loculeuses +locutions +loess +lofant +lofent +loferiez +loft +logatomes +logeant +logees +logerait +logettes +logicielles +logigrammes +logiste +logo +logogriphes +logopedies +logothete +loguames +logue +logueras +loguions +loirs +lolita +lombalgie +lombes +lombricoide +lomeenne +londoniens +longanimites +longeant +longees +longerait +longeront +longimetrie +longuement +longueurs +looks +lopins +loquetaient +loqueteux +loran +lorettes +lorgnassent +lorgner +lorgnerions +lorgnez +lorientaises +lorrain +losange +loterie +lotionna +lotionnasses +lotionnera +lotionnerons +lotirai +lotiront +lotisses +lotites +lottes +louait +louangeas +louangent +louangerez +louangez +louat +loubine +louchas +louchebemes +loucherais +loucherions +louchez +louchirait +louchissais +louchit +louer +louerions +loufiat +lougres +loukoums +loupage +loupassent +louper +louperions +loupiote +lourais +lourat +lourdames +lourdaud +lourdera +lourderiez +lourdingues +lourer +lourerions +loustic +louvaient +louvasses +louvera +louverons +louvetant +louveteau +louvetions +louvetteras +louvieroises +louvoierais +louvoya +louvoyasse +louvoyez +lovant +lovee +loverait +loviez +loyales +lozerien +lubricite +lubrifiantes +lubrifier +lubriques +lucanistes +lucide +luciferiens +lucioles +lucratives +ludions +ludologues +lues +lugea +lugeasses +lugera +lugerons +lugions +luirait +luisaient +luisent +luisisse +lulu +lumen +luminances +lumineux +luminosites +lunaisons +lunchait +lunchates +luncherent +lunchons +lunetieres +lunettieres +luo +lupiques +lurex +lushoise +lustrage +lustrant +lustrat +lustrera +lustreriez +lustriez +luta +lutasse +luteales +luteine +lutent +luteriez +lutetiums +lutheries +lutina +lutinasses +lutinera +lutineriez +lutions +luttais +luttat +lutteras +lutteuse +luxaient +luxassions +luxerent +luxmetre +luxuriances +luzerniere +lycaon +lycenide +lycopene +lydienne +lymphatiques +lymphocyte +lymphokines +lynchage +lynchassent +lyncher +lyncherions +lynchions +lyophilisat +lyophilisent +lyra +lyrasses +lyreraient +lyres +lyrismes +lysas +lysee +lyserent +lyserons +lysons +lysosomiaux +mabouls +macadamisa +macadamiser +macanaises +macaronades +maccarthysme +macchiaioli +macerais +macerat +macerees +macererez +macerons +machant +machates +machera +macherons +machiavel +machicot +machicotas +machicoter +machiez +machinales +machinates +machinees +machinerez +machinions +machisme +machoirons +machonnasse +machonnement +machonnerent +machonnons +machouillas +machouillent +machuraient +machures +maclages +maclasses +maclera +maclerons +maconnages +maconnasse +maconnent +maconnerie +maconnique +macquaient +macquassions +macqueraient +macques +macreuses +macrocheire +macroures +maculaires +maculassions +maculent +maculeriez +maculions +madecasse +madelinotes +maderiens +maderisees +maderiserez +madicole +madragues +madrepores +madrigal +maerl +maffes +mafflue +mafiosos +maganassent +maganer +maganerions +magasinage +magasiner +magasiniere +maghrebins +magiquement +magistrat +magmatiques +magnanarelle +magnanimes +magnasses +magner +magnerions +magnetisable +magnetisants +magnetiseurs +magnetites +magnetos +magnifiaient +magnifie +magnifieras +magnifiions +magnoliacees +magots +magouillas +magouillees +magouillerez +magouillez +mahaleb +maharane +mahdisme +mahonia +mahratte +maieure +maigrelet +maigrichonne +maigrir +maigririons +maigrisses +mailla +maillantes +maille +maillerais +maillet +mailletas +mailletees +maillette +mailloches +main +maintenances +maintenue +maintiendras +maintient +maintintes +maire +maisicoles +maisonnerie +maitres +maitrisames +maitriserai +maitriseront +majeste +majolique +majoral +majorassiez +majore +majoreras +majorez +majorquine +makhzen +makossa +malabeenne +malacologies +maladie +maladresses +malaimees +malaisien +malandreux +malaria +malawien +malaxaient +malaxassions +malaxera +malaxerons +malayalams +malbatie +maldiviennes +malefices +malekites +malentendu +malfaisant +malformatif +malgracieuse +malhonnetete +malienne +malikite +malinkes +malis +malles +malmena +malmenasse +malmenent +malmeneriez +malmignattes +malolactique +malouf +malpighie +malposition +malsaines +malsonnantes +maltais +maltassent +malter +malteries +maltose +maltraitait +maltraiter +malures +malvoyance +mamamouchis +mamees +mamelons +mamelukes +mamma +mammite +mammy +manadiers +manageasse +managements +managerent +managers +managuayenne +manat +manche +manchonna +manchonnera +manchotes +manda +mandant +mandarinaux +mandassiez +mandatames +mandate +mandaterais +mandatez +mandchous +mandement +manderent +mandibulate +mandole +mandorles +mandrinaient +mandrines +manege +manegeassent +maneger +manegerions +manette +manganeses +manganique +mangeaient +mangeasses +mangeons +mangeoter +mangeottes +mangerait +mangers +mangez +mangos +mangoustiers +maniabilite +maniames +maniassiez +manichordion +maniements +manieree +manieristes +manifesta +manifestas +manifeste +manifestiez +manigancait +manigancates +maniganciez +manillais +manillassiez +manillerai +manilleront +manillons +manipulables +manipulasses +manipulerait +manipuliez +manitobains +mannequinats +mannoise +manoeuvrable +manoeuvrer +manoeuvrions +manometrique +manoquait +manoquates +manoquerait +manoquiez +manouche +manquais +manquasses +manquent +manqueriez +mansa +mansuetude +mantele +mantelure +mantouane +manucurai +manucurerai +manucureront +manueline +manufacturai +manuscrits +manuterge +maoiste +mappa +mappasse +mappemonde +mapperent +mappons +maquaient +maquassions +maqueraient +maquereautee +maquerella +maquerelles +maqueront +maquettant +maquettee +maquetterent +maquettisme +maquignonnat +maquillames +maquille +maquilleras +maquilleuse +maquisardes +maraboutais +maraboutat +maraboutez +maracas +maraicher +maranta +marasques +maraudaient +maraudes +maravedis +marbrassent +marbrer +marbreries +marbrez +marc +marcassites +marcescents +marchanda +marchandasse +marchandent +marchandiez +marchantie +marchates +marcheraient +marchers +marchions +marcotta +marcottasse +marcottent +marcotteriez +mardi +marechalats +maregramme +maremoteur +mareyage +margarine +margaudaient +margauderait +margaudiez +margeais +margeat +marger +margerions +margina +marginalisai +marginas +marginee +marginerent +marginons +margotant +margotent +margoteriez +margotons +margottasse +margottera +margotterons +margoulette +margraviat +mariai +marianistes +mariaux +marierait +marieurs +marijuana +marinai +marinassiez +marinerai +marineront +mariniere +mariole +maritales +marivaudiez +marketait +marketates +marketeuses +marketterai +markettes +marlis +marmitage +marmitassent +marmiter +marmiterions +marmonnai +marmonner +marmoreennes +marmorisasse +marmorisee +marmorisons +marmottames +marmotte +marmotterais +marmotteur +marmouset +marnait +marnates +marnerait +marneurs +marocain +maronite +maronnasse +maronnera +maronnerons +maroquinages +maroquinera +maroquiniez +maroufla +marouflasse +marouflent +maroufleriez +maroutes +marquante +marquates +marquerait +marquesane +marquetas +marquetees +marquetez +marqueuse +marquisien +marraine +marranes +marrassions +marreraient +marres +marronnage +marronnerai +marronneront +marrubes +marseillais +marsouin +marsouinas +marsouiner +marsupiale +martel +martelas +martelees +marteleras +marteleur +martiennes +martyrisa +martyrisera +martyrologes +marxisait +marxisassiez +marxiser +marxiserions +marxiste +masais +mascarons +masculin +masculinisas +masculiniser +masculins +maskoutain +masqua +masquantes +masque +masqueras +masquions +massacrante +massacrates +massacrerait +massacreurs +massaient +massasses +massent +masserent +massettes +massicotai +massicoterai +massiere +massifiames +massifierais +massifiez +massives +massorete +mastabas +masteguais +masteguat +masteguerais +masteguez +masticages +mastiff +mastiquant +mastiquee +mastiquerent +mastiquons +mastoides +mastologues +masturbais +masturbat +masturbe +masturberas +masturbions +matador +matamata +matassiez +matchames +matche +matcheras +matchiches +matelas +matelassas +matelassees +matelasserez +matelassiers +matelotes +matereau +materiez +maternant +maternee +maternerai +materneront +maternisames +maternise +materniseras +maternisions +mates +maths +matierismes +matifiait +matifiassiez +matifierai +matifieront +matinai +matinas +matinee +matinerent +matinez +matir +matirions +matisses +matois +matorrals +matraquaient +matraques +matriarcal +matricages +matricasse +matricent +matriceriez +matricielles +matriculais +matriculat +matriculez +matrilocales +matronyme +matura +maturasse +maturee +maturerent +maturite +maudimes +maudiriez +maudissent +maugrabin +maugreant +maugrebine +maugrees +maurandies +mauriciens +maurrassiens +mauvais +mauviettes +maxillipedes +maximalisai +maximalisera +maximant +maximisais +maximisat +maximiserai +maximiseront +maxwells +mayennaise +mayonnaises +mazagrans +mazarinade +mazates +mazee +mazerent +mazettes +mazouta +mazoutasse +mazoutent +mazouteriez +mazurka +meandre +meats +mecanique +mecanisas +mecanise +mecaniseras +mecanisions +mecenat +mechait +mechasse +mechent +mecheriez +mechions +meconduira +meconduirons +meconduisez +meconduisit +meconiaux +meconnaitra +meconnusses +mecontentait +mecontente +mecontentiez +mecreance +medaillable +medailler +medaillions +medersa +mediamats +mediat +mediations +mediatisant +mediatisiez +mediaux +medicamentas +medicamentes +medicastres +medicinier +medieval +medimnes +mediopalatal +medirait +medisaient +medisent +medissions +meditasse +meditations +mediteraient +mediterrane +meditions +medleys +medullas +medusames +meduse +meduseras +medusions +mefiait +mefiassent +mefier +mefierions +mega +megajoule +megalo +megalomane +megalopteres +megapode +megaron +megira +megirons +megissames +megisse +megisseras +megissez +megohmmetres +megotames +megote +megoteras +megoteuse +meharee +meilleures +mejanage +mejugeas +mejugent +mejugerez +meknassie +melaient +melancolie +melangeai +melangerai +melangeront +melanines +melas +melba +meleagrines +melerai +meleront +melilot +meliphages +melisses +melliferes +mellites +melodique +melomanes +melongine +melopees +melusine +membrue +memerage +memerassent +memererai +memereront +memorable +memoriaux +memorisais +memorisat +memoriserai +memoriseront +menaca +menacas +menacees +menacerez +menade +menageas +menagement +menageras +menageront +menais +menasse +mencheviques +mendiai +mendiasse +mendiee +mendierent +mendigotai +mendigoterai +mendois +menent +meneriez +meneurs +menine +meningitique +meniscales +menologe +menopome +menotaxie +menottant +menottee +menotterent +menottons +mensonges +menstrues +mensualisent +mensuel +mentaient +mentalisais +mentalisat +mentaliserai +mentant +menteuses +mentholes +mentionnes +mentirais +mentisme +mentonnet +mentore +menuisa +menuisasse +menuisent +menuiserie +menuisieres +meo +meprenaient +meprendras +meprenions +meprisables +meprisas +meprisees +mepriserez +meprisse +meranti +mercerie +mercerisant +mercerisee +mercerisiez +mercieres +mercureuses +mercurien +merdaient +merdasses +merdera +merderons +merdions +merdoieras +merdouillat +merdouillons +merdoyassent +merdoyiez +merguez +meridionale +meringuais +meringuat +meringuerais +meringuez +merisiers +meritai +meritasse +meritent +meriteriez +merleau +merlonnaient +merlonnes +merluche +merveille +mesa +mesalliais +mesallierai +mesallieront +mesangette +mescals +mesenchyme +mesestimera +mesmeriennes +mesocolon +mesologie +mesopause +mesosphere +mesothorax +mesquinerie +messageries +messeigneurs +messianismes +messieent +messine +mestrances +mesurais +mesurat +mesureraient +mesures +mesurons +mesusas +mesuser +mesuserions +metabole +metabolisant +metabolisees +metabolisons +metacentres +metagalaxies +metaldehyde +metallifere +metallisant +metalliseurs +metallogenie +metaphorisee +metaphrases +metaplasie +metastases +metaux +meteils +meteorique +meteorisasse +meteorisee +meteorisme +methanieres +methanisant +methanisiez +methanols +methodisme +methylamines +meticals +metissa +metissasse +metissent +metisseriez +metoniens +metrage +metrassent +metrer +metrerions +metricienne +metrisations +metrologue +metrons +metrorragie +mettable +metteurs +mettraient +meubla +meublas +meublees +meublerez +meuf +meuglasse +meuglent +meugleriez +meula +meularde +meulates +meulerait +meules +meulieres +meunieres +meursault +meurtriere +meurtrirait +meurtrissais +meurtrissure +meute +mevende +mevendissent +mevendrais +mevendu +mezail +mezouza +miam +miaulais +miaulat +miaulerais +miauleur +micacee +michetonna +michetonniez +miconia +microbicides +microcebe +microcopiait +microcopie +microcopions +microcredits +microfiches +microfilmat +microfilmiez +microforme +micrographie +microlitique +micronisames +micronisez +micropsies +microsociete +microspores +microtomes +mictionnel +midrashim +miellats +miellures +mievre +mignarda +mignardasses +mignardera +mignarderons +mignon +mignotai +mignotassiez +mignoterai +mignoteront +migraines +migrants +migrateurs +migrera +migrerons +mijaurees +mijotas +mijotees +mijoterez +mijotions +milanaise +mildiousee +milices +militaire +militantes +militarisant +militarise +militarisme +militassions +militerait +militiez +millades +millenaire +millepertuis +millerandes +millesimasse +millesiment +millets +milliards +milliers +millimetrais +mimai +mimassiez +mimerai +mimeront +mimine +mimographes +mimosee +minages +minarets +minaudai +minaudassiez +minauderais +minauderont +minbar +mincir +mincirions +mincisses +mincolettes +mineraient +mineralisat +mineralisent +mineralogie +minerent +minerviste +mineur +miniature +minibars +minidose +minijupes +minimaux +minimisa +minimisasses +minimisent +minimiseriez +minimums +ministeres +ministres +minivagues +minoens +minorant +minorat +minoree +minorerent +minorisa +minorisasses +minorisera +minoriserons +minorites +minotes +minouchais +minouchat +minoucherais +minouchez +minskois +minutages +minutasse +minutent +minuterie +minutie +minutons +mirabelle +miraculee +mirages +mirasses +mire +mirerait +mirettes +mirions +mirmillons +miroita +miroitas +miroitees +miroiteras +miroitez +mirontons +misaines +misanthropes +misassions +misees +miserables +miseres +miserions +misogamie +misons +missiez +missionnes +mister +mistral +mitaines +mitasse +mitent +miteriez +mithracisme +mitigeaient +mitigeraient +mitiges +mitonnait +mitonnates +mitonnerait +mitonniez +mitoyennes +mitrailles +mitraillons +mitrons +mixais +mixat +mixerais +mixes +mixons +mixtionnes +mnemes +moabite +mobile +mobilisables +mobilisasses +mobiliserait +mobilisiez +mobilophones +mochard +modal +modalisas +modalise +modaliseras +modalisions +modelages +modelasses +modelera +modelerons +modelisa +modelisasses +modeliserait +modelisiez +modenais +moderais +moderassent +moderato +moderera +modererons +modernisee +modernisme +modestement +modifia +modifiantes +modificateur +modifie +modifieras +modifiions +modula +modulant +modulassiez +module +moduleras +modulions +moelleux +mofette +mofflasse +mofflent +moffleriez +moflai +moflassiez +moflerai +mofleront +mogholes +mohican +moierai +moies +moineries +moirais +moirat +moirerais +moireur +mois +moisas +moisees +moiserez +moisiez +moisirent +moisissant +moisit +moissonnages +moissonnera +moissonnons +moitir +moitirions +moitisses +mojitos +molard +molardassent +molarderai +molarderont +molasses +molecule +molestais +molestat +molesterais +molestez +moletait +moletates +moletoir +moletteras +molieresques +mollachue +mollardaient +mollarderait +mollardiez +mollasson +molletiere +molletonnas +molletonnent +molletonnons +mollira +mollirons +mollissent +molossoides +molybdenes +mome +momes +momifiais +momifiat +momifierai +momifieront +mominette +monachismes +monadologies +monarchisai +monarchisez +monarque +monazite +monda +mondanite +mondates +monderait +mondeuses +mondialisait +mondialisiez +mondiez +monemes +monetisais +monetisat +monetiserai +monetiseront +mongolienne +monial +moniliforme +monition +monitorais +monitorat +monitores +monnaie +monnaierions +monnayaient +monnayes +monoamine +monobloc +monocameraux +monocepages +monocle +monoclonales +monocorps +monocylindre +monodoses +monogataris +monogrades +monoide +monokinis +monologuerai +monomaniaque +mononuclee +monoplaces +monopoleuses +monopolise +monopolisons +monorchidie +monosemies +monoskis +monostables +monosyllabes +monotheismes +monotones +monotypes +monoxyles +monsignor +monstrations +monstrueux +montagnaise +montagneuse +montants +montbeliarde +montent +monteurs +monticole +montjoie +montra +montrant +montrealais +montrerais +montreusiens +montures +moores +moquassent +moquer +moqueries +moquettait +moquettates +moquetterait +moquettiez +moquez +moraillais +moraillat +moraillerais +moraillez +morale +moralisante +moralisates +moralisent +moraliseriez +moralisiez +morasses +morbide +morcelable +morcelassent +morcelerent +morcellerai +morcelles +mordait +mordancas +mordancees +mordancerez +mordant +mordes +mordicus +mordillait +mordillates +mordilles +mordissent +mordorais +mordorat +mordorerais +mordorez +mordrait +mordue +morene +morfalait +morfalates +morfalerait +morfaliez +morfilais +morfilat +morfilerais +morfilez +morflames +morfle +morfleras +morflions +morfondez +morfondre +morfondus +morguai +morguassiez +morguera +morguerons +moribondes +morigenait +morigenates +morigenerait +morigeniez +morion +mormons +mornifles +morpheme +morphinomane +morsures +mortaisait +mortaisates +mortaiserait +mortaiseurs +morteau +mortifere +mortifiantes +mortifierais +mortifiez +morues +morvandelles +morveuses +mosaiquames +mosaique +mosaiqueras +mosaiquions +moscoutaire +mosette +mossies +motards +motifs +motionnait +motionnates +motionnerent +motionnons +motivantes +motivation +motivera +motiverons +motobineuse +motocultures +motoneiges +motopaveurs +motorisai +motoriser +motorship +motrices +mottant +mottee +motterent +mottions +mouchaient +moucharda +mouchardasse +mouchardent +mouchas +mouchees +moucherez +moucheronner +moucheta +mouchetasses +mouchetes +mouchetterai +mouchettes +mouchoir +moudjahidin +moudrez +moufeta +moufetasses +moufetes +mouflage +mouflassent +moufler +mouflerions +mouflions +mouftas +moufter +moufterions +mouillait +mouillassait +mouillat +mouillerai +mouillerions +mouillez +mouises +moulage +moulants +moulee +moulerent +mouleuses +moulinaient +moulines +moulinier +moulins +moulue +moulurames +mouluration +moulurerais +moulurez +moulussent +moundas +mourants +mouroirs +mourres +mourussent +mouscronnois +mousqueterie +moussaillon +moussants +mousseau +mousseraient +mousseront +mousson +moustaches +moustier +moutardaient +moutardera +moutarderons +moutier +moutonnames +moutonnera +moutonneriez +moutonniere +mouvaient +mouvants +mouvementer +mouvez +mouvras +moxas +moyas +moyees +moyennames +moyenne +moyennerait +moyenniez +moyeux +mozambicains +mozzarellas +muance +muates +muchassent +mucher +mucherions +mucoracee +mudejares +muerai +mueront +muezzins +muftis +mugir +mugirions +mugissants +mugit +muguetant +muguetee +muguette +mulardes +mulatre +mulet +mulhousien +mullides +multibrins +multicolore +multiforme +multilateral +multimetre +multinorme +multiple +multiplexais +multiplexez +multipliait +multipliates +multiplierai +multipolaire +multivariee +mumuse +municipale +municipaux +munificents +munirez +munisse +munitionna +munitionner +muntjac +muqueuses +murailla +muraillasses +muraillent +murailleriez +murait +murant +mure +murera +mureriez +murettes +murgeant +murgees +murgerent +murgions +muriers +murira +murirons +murissantes +murissiez +murmurais +murmurasses +murmurera +murmurerons +murrhes +musagete +musard +musardassent +musarderai +musarderions +musardions +musassions +muscadier +muscari +muschelkalk +muscinales +musclant +musclee +musclerent +musclons +musculatures +museaux +museifias +museifiees +museifierez +musela +muselasse +museler +muselle +musellerez +museographie +muser +muserions +musez +musicassette +musiquas +musiquees +musiquerez +musiquions +musquais +musquat +musquerais +musquez +mussant +mussee +musserent +mussifs +mussolinien +musts +mutables +mutait +mutassiez +mutationnels +mute +muteras +mutila +mutilas +mutilation +mutilerai +mutileront +mutinais +mutinat +mutinerais +mutineront +mutisme +mutualisames +mutualisez +mutuel +mutules +myatonies +mycenienne +mycoplasmes +mycorhiziens +mydriatique +myelinisee +myelogrammes +myelopathies +mylonite +myocardiques +myoglobines +myologiques +myopathes +myopotames +myosite +myriametres +myrmidons +myroxylons +myrtiformes +mystagogues +mysticismes +mystifiames +mystifiee +mystifierent +mystifions +mythifiait +mythifiates +mythifies +mythographie +myxovirus +nabateennes +nacrai +nacrassiez +nacrerai +nacreront +nadiral +nagaika +nageait +nageates +nageotais +nageotat +nageoteras +nageotions +nagerez +nagez +nahuas +nain +nais +naissant +naissiez +naitre +naivetes +namuroise +nanceienne +nanifiai +nanifiassiez +nanifierai +nanifieront +nanisait +nanisates +naniserait +nanisiez +nans +nanterrois +nantiraient +nantis +nantissez +naos +naphtaline +naphtols +napolitains +nappas +nappees +napperez +nappons +naquites +narcissiques +narcotines +narghile +narguant +narguee +narguerent +narguiles +narrai +narrassiez +narration +narratrice +narrerais +narrez +nasale +nasalisasse +nasalisee +nasaliserent +nasalisons +nase +nasillait +nasillasses +nasillent +nasilleriez +nasilliez +naskapis +nasonnassent +nasonnements +nasonnerez +nasse +natalites +natifs +nationalisme +nativement +natoufiennes +natrum +nattant +nattee +natterent +nattieres +naturelle +naturopathes +naufrages +naupathie +nauseabonde +nautique +navajo +navarque +navel +navettames +navette +navetterez +navettez +navicule +navigateur +naviguait +naviguates +naviguerent +naviguons +navrais +navrasses +navrent +navreriez +nays +nazcas +nazifiait +nazifiates +nazifies +nazisme +neantisaient +neantisera +neantiserons +nebkha +nebulisaient +nebulisera +nebuliserons +nebulosites +necessitant +necessitat +necessiteuse +necrobie +necrologue +necromantes +necrophobe +necros +necrosassent +necroser +necroserions +necrotique +neems +nefle +negativite +negligeable +negligences +negligerais +negligez +negociai +negociasse +negociations +negocies +negriers +neige +neigeoterait +nelombo +nematocystes +nemertiens +nene +neoblaste +neoclassique +neodarwinien +neofasciste +neogrecques +neoliberales +neolocales +neologisait +neologisates +neologisme +neonatal +neonazie +neopilina +neoplastie +neoprene +neorural +neotenies +nepe +nepetas +nephelions +nephrites +nephrologues +neptunismes +nerf +neroniennes +nerveuse +nervosisme +nervurait +nervurates +nervurerait +nervuriez +nestoriennes +netsuke +nettoient +nettoierons +nettoyais +nettoyasses +nettoyes +networks +neumatique +neurogene +neurologiste +neuropathies +neurotomies +neuston +neutralisent +neutraliste +neutron +neuvaine +neversoise +nevralgies +nevritiques +nevropathies +nevrosiques +newton +ngultrums +niaisa +niaisasse +niaisement +niaiserez +niaiseux +niameyens +niassent +nibard +nichai +nichassiez +nicherai +nicheront +nichions +nickelages +nickelasses +nickeles +nickellera +nickelleront +nicois +nicosienne +nicotinismes +nictitations +nidifia +nidifiasses +nidifierent +nidifions +nidwaldiens +niellages +niellasses +niellera +niellerons +niellons +nierai +nieront +nifes +nigerian +nihilisme +nille +nimbai +nimbassiez +nimberai +nimberont +nimoises +niolo +nippa +nippasses +nippera +nipperons +nipponnes +niquant +niquedouille +niquerait +niquiez +nitouche +nitrantes +nitratai +nitratassiez +nitrater +nitraterions +nitree +nitrerent +nitrez +nitrifiant +nitrifiat +nitrifiees +nitrifierez +nitrile +nitrogene +nitrosaient +nitrosasses +nitrosent +nitroseriez +nitrosyle +nitrurant +nitrurations +nitrurerait +nitruriez +nivations +nivelages +nivelasses +niveles +nivelle +nivellerez +niveole +nizere +nobelisait +nobelisates +nobeliserait +nobelisiez +noblaillonne +noca +nocasses +noceraient +noces +nocicepteur +nocifs +noctules +nodaux +noel +noeud +noierez +noiraude +noircira +noircirons +noircissent +noircissure +noises +nolisai +nolisassiez +noliser +noliserions +nom +nomadisant +nomadiserait +nomadisiez +nombraient +nombrassions +nombreraient +nombres +nombrilistes +nominal +nominalisant +nominalise +nominalisme +nominasses +nomineraient +nomines +nommai +nommassiez +nommera +nommerons +nomographes +non +nonce +nonchaloirs +nonnettes +nonuplai +nonuplassiez +nonuplerai +nonupleront +nopal +nordet +nordirai +nordiront +nordissiez +norien +normal +normalisee +normalisons +normasse +normatives +normees +normerez +normographes +norvegien +nosocomiaux +nosologiques +nostocs +notaire +notariale +notasse +notations +notera +noterons +notifiaient +notifiee +notifierent +notifions +notoirement +notules +nouassions +noucs +noueraient +noues +nougatine +noulets +noumenes +nourriceries +nourrira +nourrirons +nourrissiez +nouures +nouvellistes +novasse +novations +novelisa +novelisasses +novelisent +noveliseriez +novelles +novellisees +novelliserez +novembre +noverent +novices +novions +noya +noyant +noyauta +noyautasse +noyautent +noyauteriez +noyautiez +noyerent +nuages +nuames +nuancassent +nuancements +nuancerez +nuancions +nuates +nubilites +nucelles +nuclearisas +nuclearisee +nucleariste +nucleine +nucleolyse +nudismes +nuer +nuerions +nuira +nuirons +nuisent +nuisiez +nuisit +nuitees +nulle +numeraires +numerative +numerisaient +numerisera +numeriserons +numerologie +numerotai +numeroter +numerotions +nummulite +nunchaku +nuons +nuraghes +nurserys +nutriciers +nycthemeraux +nymphalide +nymphees +nymphose +oasis +obedienciers +obeira +obeirons +obeissantes +obeites +oberais +oberat +obererais +oberez +obiers +objectait +objectassiez +objectera +objecterons +objections +objectivas +objective +objectiviez +objectrice +oblates +oblature +obligea +obligeante +obligeates +obligerait +obligiez +obliquasse +obliquer +obliquerions +oblitera +obliterasses +oblitererait +obliteriez +obnubilais +obnubilat +obnubilerai +obnubileront +obombraient +obombres +obscurcir +obscures +obsedait +obsedassiez +obsederai +obsederont +obseques +observable +observantin +observat +observatrice +observerais +observez +obsolescence +obstetrical +obstina +obstinasses +obstinement +obstinerez +obstineux +obstruames +obstructif +obstruer +obstruerions +obtemperiez +obtenir +obtenues +obtiendrions +obtins +obturaient +obturassions +obturee +obturerent +obturons +obusiers +obvenu +obviais +obviat +obviendriez +obviera +obvierons +obvinssent +obwaldiens +occasionnais +occasionnat +occasionner +occidentale +occiputs +occitaniste +occluez +occluras +occlusale +occlusives +occultaient +occultera +occulterons +occultons +occupants +occuperai +occuperont +occurrentes +oceanienne +ocellee +ochronoses +ocrassent +ocrer +ocrerions +ocrons +octanoique +octave +octaviasse +octavient +octavieriez +octavions +octidi +octogonale +octosyllabes +octroierait +octroyai +octroyassiez +octroyez +octuplait +octuplates +octuplerait +octupliez +ocules +oculus +odeon +odologies +odoriferante +odorisants +odorisations +odoriserait +odoriseurs +oecumeniques +oedematiees +oedipiennes +oeilleton +oeilletonnas +oeilletonnes +oekoumene +oenolisme +oenometrie +oenothera +oesophage +oestres +oestrones +oeuvra +oeuvrasses +oeuvreraient +oeuvres +offensaient +offensassent +offenser +offenserions +offensions +offerts +officialisai +officiasse +officielle +officierait +officiers +officinale +offrandes +offrez +offriras +offrisses +offshore +offusquasse +offusquent +offusqueriez +oflags +ogivale +ogresses +oidies +oignent +oignisses +oignons +oindre +ointe +oiselames +oisele +oiseleuse +oisellent +oiselleriez +oisifs +ojibwa +okra +olecranien +oleicole +oleifiant +olenekien +olfaction +olibrius +oligistes +olivacee +olivaisons +olivetain +oliveuses +olographe +olympiennes +ombellale +ombelliforme +ombilique +ombrageaient +ombrages +ombrais +ombrat +ombrerai +ombreront +ombriennes +ombudsmans +omettait +omettrai +omettront +omissent +omnicolore +omnipotent +omnipresente +omnium +onagracees +onanismes +onciales +oncologie +oncques +ondatras +ondine +ondoierai +ondoies +ondoyants +ondoyee +ondulai +ondulasse +ondulatoires +ondulerait +onduleurs +onereuses +onglier +onguiforme +onirisme +online +ontogeneses +onychomycose +onzain +oogenese +oolitique +opacifia +opacifias +opacifie +opacifieras +opacifiions +opalescence +opalisa +opalisasses +opalisent +opaliseriez +opaques +operables +operantes +operateur +operatoire +opere +opereras +operez +ophidiennes +ophiolitique +ophiuride +opiacas +opiacees +opiacerez +opiat +opinames +opine +opineras +opiniatrai +opiniatrera +opinion +opodeldoch +opotherapie +opportunes +opposantes +oppose +opposeras +opposions +oppressai +oppressasse +oppressent +oppresseriez +oppressifs +opprimait +opprimassiez +opprimerai +opprimeront +opsomane +optaient +optassions +optera +opterons +optimal +optimalisas +optimalisee +optimaux +optimisees +optimiserez +optimismes +optionnels +optometriste +opulences +opuscule +orageux +oralisais +oralisat +oraliserais +oralisez +orangea +orangeasse +orangent +orangeras +oranges +orant +oratorio +orbiculaires +orbitales +orbitates +orbiterais +orbiteur +orcanette +orchestrait +orchestraux +orchestriez +orchidees +ordalique +ordinands +ordonna +ordonnancat +ordonnances +ordonnassent +ordonne +ordonneras +ordonnions +ordure +orees +oreillettes +orfevrerie +organeau +organicismes +organisant +organiser +organismes +organogenese +organsines +organza +orgeats +orgie +orichalque +orientais +orientates +orienter +orienterions +orientions +origans +originale +originasses +originelle +originerait +originiez +orioles +orleanaises +orme +ormoie +ornant +ornee +ornemental +ornementer +orner +ornerions +ornions +ornithoses +orogenese +orometrie +oropharynx +orphelinat +orpheons +orpins +orthocentre +orthodoxe +orthogenese +orthonormes +orthopnees +orthoptistes +ortolans +orwelliens +osais +osat +oscarisames +oscarise +oscariseras +oscarisions +oscillais +oscillasses +oscillerent +oscines +osee +oseraies +oses +osirien +osmique +osmose +osseines +ossianiques +ossifiais +ossifiat +ossifierai +ossifieront +ossues +osteites +ostensions +osteoclasies +osteogenies +osteopathies +osteoporoses +ostos +ostracisait +ostracisates +ostracisiez +ostrakon +ostreide +ostrogoths +otai +otarie +ote +oteras +othellos +otocyons +otologiques +otosclerose +ottaviennes +ou +ouais +ouatage +ouatassent +ouater +ouateries +ouatiez +ouatinassent +ouatiner +ouatinerions +oubli +oublias +oubliees +oublierez +oublieux +ouches +ouf +ougriens +ouighoures +ouillaient +ouillassions +ouilleraient +ouillerons +ouir +oulipiennes +ouolof +ouraques +ourdirait +ourdissages +ourdisseuse +ourdou +ourlait +ourlates +ourlerait +ourlets +ourse +oust +outarde +outillai +outillassiez +outillerai +outilleront +outils +outrage +outrageants +outragees +outragerent +outrageux +outrances +outrassiez +outrepassait +outrepasse +outrepassons +outreriez +outrons +ouverture +ouvrageaient +ouvrages +ouvrames +ouvrassions +ouvrera +ouvrerons +ouvrieres +ouvrira +ouvrirons +ouvroir +ovaires +ovalisai +ovalisassiez +ovaliser +ovaliserions +ovarien +ovationnai +ovationnerai +overdose +ovicule +ovines +ovni +ovoidal +ovovivipares +ovulames +ovulation +ovulerais +ovulez +oxalide +oxfordien +oximes +oxycoupages +oxydai +oxydase +oxydatifs +oxydera +oxyderons +oxydons +oxygenables +oxygenas +oxygenateur +oxygenerai +oxygeneront +oxylithes +oxysulfures +oyat +ozeneuses +ozonait +ozonates +ozonera +ozonerons +ozonisa +ozonisasses +ozonisee +ozoniserent +ozonisiez +pacagea +pacageasses +pacagera +pacagerons +pacas +pachalik +pachtounes +pachyures +pacifiants +pacifiera +pacifierons +pacifisme +packageurs +pacquages +pacquasses +pacquera +pacquerons +pacsaient +pacsassions +pacseraient +pacses +pactisaient +pactiserait +pactisiez +paddings +padines +padous +pagaient +pagaierons +paganisaient +paganises +pagayais +pagayat +pagayeras +pagayeuse +pageais +pageat +pageot +pageotassent +pageoter +pageoterions +pagera +pagerons +pagiez +paginassent +paginees +paginerez +paginions +pagnotaient +pagnotes +pagre +paiches +paient +paieriez +paillais +paillardames +paillarde +paillardiez +paillassiez +paillat +paillerais +pailles +pailletant +pailletee +pailletiez +pailleuse +paillonnes +pain +pairesses +paisiblement +paissants +paissons +paitriez +pal +palabrasse +palabrera +palabrerons +palabrons +palaisiennes +palangres +palanquames +palanque +palanqueras +palanquin +palatales +palatalisent +palatiale +palatres +palee +paleologue +paleozoique +pales +palettisai +palettiser +paletuvier +palichons +palifiai +palifiassiez +palifier +palifierions +palilalie +palirait +palissada +palissadera +palissages +palissantes +palisse +palisseras +palissions +palissonner +palit +palladium +palliaient +palliassions +pallicares +palliera +pallierons +palliums +palmames +palmassions +palmera +palmeriez +palmiers +palmipartie +palmiseques +palmitiques +palombieres +palotaient +palotassions +paloteraient +palotes +palourde +palpais +palpat +palpees +palperez +palpiez +palpitantes +palpitation +palpiteras +palpitions +palucha +paluchasses +paluchera +palucherons +palude +paludieres +paludologue +palynologue +pamasse +pament +pameriez +pamons +pampilles +panacee +panachames +panache +panacheras +panachions +panameen +panamien +panarde +panat +panatheniens +pancanadiens +pancreas +pandas +panegyrique +panelistes +paneree +paneteries +panic +panier +panifiais +panifiat +panifierai +panifieront +paniquai +paniquarde +paniquates +paniquerait +paniquiez +panjabis +panneautage +panneauter +panneaux +pannicule +panossai +panossassiez +panosserai +panosseront +panpsychisme +pansames +panse +panserais +panseur +panslavismes +pantacle +pante +pantelants +panteler +pantellerais +pantene +pantheismes +pantheoniser +pantieres +pantois +pantouflard +pantouflat +pantoufleras +pantoufliere +pantys +paonneaux +papale +papautes +papayers +papelards +papesse +papette +papilionees +papillifere +papillonnera +papillons +papillotante +papillotates +papillotes +papiste +papotaient +papotassions +papoterait +papotiez +papous +papyrologies +paquage +paquassent +paquees +paquerette +paquetage +paquetassent +paqueterent +paquets +paquetteriez +par +parabenes +paraboliques +parachevais +parachevat +paracheverai +parachutait +parachutera +parachutons +paradaient +paradassions +paraderait +paradeurs +paradis +paradoxale +parafait +parafates +paraferait +parafeurs +paraffinant +paraffinee +paraffinons +parafoudre +paraissant +paraitrai +paraitront +parallaxe +paralogique +paralysait +paralyserai +paralyseront +parames +parametrames +parametre +parametreras +parametrions +paramnesie +parangonnage +parangonnera +paranoiaques +paranthrope +paraphates +parapherait +parapherons +paraphraser +paraphrasons +parapodes +paraquats +parasitaires +parasites +parasitoide +parasol +parassions +parates +paravent +parce +parcellarisa +parcellarise +parcellat +parcellerai +parcelleront +parcheminant +parcheminees +parcheminez +parchets +parcometre +parcoures +parcourrait +parcourue +parcourut +pardonnai +pardonnerai +pardonneront +paredre +pareils +parementant +parementee +parementons +parent +parente +parents +parere +paresies +paressassent +paresserai +paresseront +paresthesie +pareuse +parfaisions +parfassiez +parferions +parfilames +parfile +parfileras +parfilions +parfit +parfondes +parfondras +parfondues +parfumant +parfumee +parfumerent +parfumeurs +pari +pariames +pariat +parierais +parietaire +pariez +paripennees +paritaire +parjurait +parjurates +parjurerait +parjuriez +parkerisames +parkerisez +parlait +parlassent +parlementa +parlementas +parlementera +parlerai +parleront +parloir +parlotames +parlote +parloterez +parlotte +parmentiers +parnassienne +parodias +parodiees +parodierez +parodique +parodontie +paroisses +paroles +paronyme +parotides +paroxysmale +paroxytons +parquais +parquat +parquerai +parqueront +parquetames +parquete +parqueteuse +parquets +parquier +parrainages +parrainasses +parrainera +parrainerons +parrainons +parsemaient +parsemes +parsisme +partageait +partagerai +partageront +partaient +partenaire +parterres +partialites +participatif +participerai +particulaire +particuliers +partinium +partirent +partissiez +partitifs +partitionnez +partousai +partousas +partouser +partousions +partouzard +partouzat +partouzeras +partouzeuse +parturitions +paruline +parurieres +parution +parvenons +parviendras +parvient +parvintes +pascals +pashmina +pasquinais +pasquinat +pasquinerais +pasquinez +passacailles +passai +passasse +passavants +passementai +passements +passepoilas +passepoilent +passeports +passereau +passerons +passeurs +passiflore +passionistes +passionnante +passionnates +passionner +passivas +passive +passiverait +passiviez +pastel +pasteller +pastels +pasteurisait +pasteurises +pastichaient +pastiches +pastillage +pastiller +pastillions +pastissas +pastissees +pastisserez +pastoral +pastorienne +pataca +patagon +pataques +patas +pataugas +pataugeant +pataugeoire +pataugerent +pataugeuses +pate +patelinait +patelinates +patelinerait +patelines +patene +patentai +patentassiez +patenterai +patenteront +pater +paternelle +pateusement +pathogene +pati +patientais +patientat +patienterais +patientez +patinai +patinassiez +patinerai +patineront +patinions +patiraient +patis +patissas +patissees +patisserez +patissiere +patites +patoisant +patoisat +patoiseras +patoisions +patouillais +patouillerai +patraques +patriarche +patricien +patrigotai +patrigotez +patrilocale +patrimonios +patriotisme +patronal +patronnage +patronner +patronniers +patrouilla +patrouilles +patte +pattieres +paturables +paturasse +paturent +patureriez +paturon +paulien +pauliste +paumant +paumee +paumerait +paumier +paumons +paumoyassent +paumoyerent +pauperisera +paupieres +pausas +pauser +pauserions +pauvres +pavage +pavanais +pavanat +pavanerais +pavanez +pavassions +pavera +paverons +paveuses +pavillonneur +pavloviens +pavoisasse +pavoisement +pavoiserent +pavoisons +paya +payantes +paye +payerais +payeur +paysagee +paysagistes +payses +peans +peaufinaient +peaufines +pebrine +pecari +peccante +pechames +pechblende +pecherais +pecheriez +pecheuse +peclota +peclotasses +peclotes +pecore +pectens +pectorale +pecunes +pedagogues +pedalas +pedalees +pedalerez +pedalez +pedanterie +pedegeres +pedestre +pedicellaire +pediculees +pedieuse +pediments +pedologies +pedonculee +pedzant +pedzent +pedzeriez +pedzouilles +pegots +peguas +peguees +peguerez +peguiez +peignais +peignat +peignerais +peignette +peignimes +peignites +peinaient +peinas +peindraient +peine +peineras +peinions +peinturage +peinturer +peinturlurai +pejorasse +pejorations +pejorerai +pejoreront +pekin +pekins +pelagie +pelaient +pelantes +pelasgienne +pelat +peleennes +peleras +pelerions +pelions +pellaient +pellassions +pelleraient +pelles +pelletant +pelletee +pelleteuses +pelletterait +pelliculage +pelliculas +pelliculees +pelliculerez +pelliculiez +pelobate +pelota +pelotaris +pelotee +peloterent +peloteuses +pelotonnames +pelotonne +pelotonnez +peltas +peluchai +peluchassiez +pelucherai +pelucheront +pelures +pelvimetres +pemphigoides +penalisait +penaliser +penalite +penates +penchais +penchassions +pencheraient +penches +pendaient +pendarde +penderies +pendillant +pendillent +pendilleriez +pendimes +pendites +pendouillait +pendouille +pendrai +pendront +pendulais +pendulat +pendulerais +pendulette +pendus +penetrable +penetrante +penetrates +penetrera +penetrerons +penibilites +penicillees +peninsulaire +penitent +pennies +penny +pensables +pensas +pensees +penserez +penseuses +pensionnerai +pensons +pentagones +pentapetales +pentarques +pentatome +pentecotes +penthodes +pentoses +pentys +peperin +pepiames +pepie +pepieras +pepiions +pepite +peppermints +peptidique +pequenaudes +pequiste +perca +percalines +percassiez +percept +perception +percerette +percette +percevait +percevrais +perchage +perchassent +perchees +percherez +percheurs +perchlorates +percide +percluses +percola +percolasses +percolee +percolerent +percolons +percusse +percussives +percutanees +percutassiez +percuterai +percuteront +perdable +perdent +perdisses +perdra +perdriez +perdues +perduras +perdurer +perdurerions +peregrin +peregriner +peremptions +perennisa +perennisent +perennites +perfectibles +perfections +perfolie +perforait +perforassiez +perfore +perforeras +perforez +performances +performera +performerons +performions +perfusames +perfuse +perfuseras +perfusion +perianthe +periboles +perichondres +pericliterai +periderme +peridurales +perigordien +perihelies +perimais +perimat +perimerais +perimetre +perinatales +perineaux +peripheries +periphrasent +periphs +perirait +perissez +peritoine +periurbain +perlait +perlassent +perlees +perlerez +perlinguale +perlot +permanentant +permanentees +permanganate +permettaient +permettons +permettrions +permise +permissions +permselectif +permutaient +permutassent +permutees +permuterez +pernicieuse +peronier +perorait +perorates +perorerent +peroreuses +peroxydai +peroxydees +peroxyderez +peroxysome +perpetrant +perpetrerait +perpetriez +perpetuant +perpetuer +perpignanais +perre +perruqua +perruquasses +perruquera +perruquerons +perruquons +persecutais +persecutat +persecuteur +perseides +perseverance +persevererai +persienne +persifflant +persifflee +persiflais +persiflat +persiflerais +persifleur +persillade +persiller +persillerez +persils +persistances +persisterait +persistiez +personnage +personnifia +personnifiat +perspicaces +persuadames +persuade +persuaderas +persuadions +persulfates +pertinent +perturbai +perturbasse +perturbes +peruvienne +perversions +pervertis +pervibrage +pervibre +pervibreras +pervibrions +pesait +pesasse +pesent +peseriez +peseurs +pesons +pessiere +pestait +pestates +pesterent +pestez +pestilentiel +petage +petale +petanqueurs +petaradai +petaradasse +petaradera +petaraderons +petardaient +petardes +petas +petassames +petasse +petasseras +petassions +petchi +petees +peterez +peteuses +petillance +petillassiez +petillerai +petilleront +petions +petitesses +petitionnant +petitoire +petochards +petoncle +petouillas +petouiller +petrarquisa +petrarquisas +petrel +petrifiai +petrifiasse +petrifiee +petrifierent +petrifions +petriras +petrissage +petrisseurs +petrochimies +petrogeneses +petrolai +petrolassiez +petrolerai +petroleront +petroliere +petulances +petunait +petunates +petunerent +petuniez +peucedans +peuls +peuplames +peuple +peupleraient +peupleront +peureusement +pezes +pfut +phacos +phagocytage +phagocytas +phagocytees +phagocyterez +phagocytose +phalangettes +phalansteres +phalline +phanatrons +phanotrons +phantasmer +pharamineux +pharaonne +pharisaismes +pharynge +phasmes +phellogenes +phenetique +pheniquees +phenolate +phenomenal +phenomenes +phenoplastes +pheromones +philibegs +philologue +philosophent +phishings +phlebologues +phlegmon +phobies +phocidienne +pholcodine +phonation +phonemes +phonetiques +phoniques +phonogramme +phonolithe +phonologue +phonotheques +phosgene +phosphatant +phosphatates +phosphates +phosphorasse +phosphoremie +phosphorisme +phosphoryle +phosphure +photochrome +photocopiee +photocopiez +photogene +photoglyptie +photometre +photoniques +photopile +photosphere +phototypies +phrasait +phrasates +phrasera +phraserons +phrasons +phrenologies +phrygiens +phtirius +phtisiques +phycomycetes +phyllie +phylloxera +phylum +physiatries +physostigma +phytochimies +phytogene +phytophage +phytopte +piaculaires +piaffantes +piaffe +piafferas +piaffeurs +piaillaient +piaillasse +piaillent +piaillerie +piailleuses +pianistes +pianotage +pianotassent +pianoter +pianoterions +piapiatai +piapiaterais +piapiatez +piaulaient +piaulassions +piauleraient +piaules +pibales +picaillon +picarel +picassiens +picholines +pickup +picolait +picolates +picolerait +picoleurs +picons +picorassent +picorer +picorerions +picossa +picossasses +picossera +picosserons +picotages +picotasses +picotent +picoteriez +picotons +picride +picrocholins +pidgin +piedmont +piegea +piegeasse +piegeons +piegeriez +piegez +piercai +piercassiez +piercerai +pierceront +piercions +pierree +pierriers +pietaille +pietassiez +pieter +pieterions +pietinais +pietinasses +pietinent +pietineriez +pietions +pietonnieres +pietonnisees +pietrain +pieutai +pieutassiez +pieuterai +pieuteront +piezes +pifas +pifees +piferez +piffais +piffat +pifferais +piffez +pifons +pigeas +pigent +pigeonnant +pigeonnat +pigeonnerai +pigeonneront +pigera +pigerons +pigmentai +pigmentees +pigmenterez +pigments +pignames +pignatelles +pignerais +pignez +pignochas +pignochees +pignocherez +pignon +pigouilla +pigouillera +pilage +pilas +pilau +piler +pilerions +pileux +pilipino +pillant +pillassions +pilleraient +pilles +pilocarpes +pilonnait +pilonnates +pilonnerait +pilonniez +piloselle +pilotai +pilotassiez +piloterai +piloteront +pilou +pilules +pimenta +pimentasse +pimentent +pimenteriez +pimpant +pinacles +pinaillames +pinaille +pinaillerez +pinaillez +pinastre +pincant +pincassions +pinceautais +pinceautat +pinceautez +pincements +pincerez +pinceuse +pincons +pindarisames +pindarise +pindariserez +pindarismes +pinene +pingres +pinnules +pinson +pintaient +pintassions +pinteraient +pintes +pintochant +pintochent +pintocheriez +pinyin +piochant +piochee +piocherent +piocheuses +pioncaient +pioncassions +pionceraient +pionces +pionnai +pionnassiez +pionnerais +pionnez +piornai +piornassiez +piornerais +piornez +pipait +pipates +pipeline +piperaient +piperiez +pipes +pipides +pipit +piquage +piquants +piquee +piqueniques +piquepoul +piquerez +piquetage +piquetassent +piqueterent +piquets +piquetteriez +piquez +piquouze +pirataient +piratassions +pirateraient +piraterons +pires +pirojoks +pirouettames +pirouette +pirouetteras +pirouettions +piscicole +piscine +pisiforme +pissa +pissasse +pissees +pisserais +pissette +pissoir +pissotas +pissoter +pissoterions +pissou +pistaient +pistasse +pistent +pisteriez +pistiez +pistolait +pistolates +pistolerait +pistoles +pistoliez +pistonnait +pistonnates +pistonnerait +pistonniez +pitbull +pitchouns +pitonna +pitonnasse +pitonnent +pitonneriez +pitonnions +pitpit +pittoresques +pituitames +pituite +pituiterez +pituitiez +pivoines +pivotantes +pivote +pivoterais +pivotez +pixelisait +pixelisates +pixelises +pixellisames +pixellisez +pizzeria +placai +placardai +placarderai +placarderont +placardisais +placat +placements +placentines +placerez +placettes +placidites +placota +placotasse +placotent +placoteriez +placotez +plafonnage +plafonnants +plafonnee +plafonnerait +plafonneurs +plagaux +plagiames +plagiats +plagierait +plagiiez +plaidables +plaidas +plaidees +plaiderez +plaidez +plaies +plaignardes +plaignis +plaindra +plaindrons +plaintive +plaire +plaisamment +plaisanciers +plaisantasse +plaisantent +plaisanterie +plaisantins +plaisir +planaire +planchai +planchassiez +plancheiais +plancheiat +plancheiez +plancherait +planchette +plancton +planees +planerais +planetaire +planetoide +planeuse +planifiaient +planifiee +planifierent +planifions +planipenne +planneur +planorbes +planquassent +planquer +planquerions +plansichters +plantains +plantasse +plantee +planterent +planteuses +plants +plaquai +plaquassiez +plaquerez +plaqueur +plaquons +plasmides +plasmifier +plasmique +plasmodium +plasticages +plastifiant +plastifiat +plastifierai +plastiquages +plastiquer +plastiquions +plastronnais +platanaie +platebandes +plateresque +platiere +platinames +platine +platineras +platinifere +platinisas +platinisees +platiniserez +platinite +platodes +platra +platrasse +platrent +platrerie +platrez +playmate +plebe +plebiscitat +plebiscitiez +pleiades +plenier +pleonastique +plethorique +pleural +pleurardes +pleuraux +pleurerait +pleuresies +pleurite +pleurnichant +pleurnichat +pleurniches +pleutres +pleuviota +pleuvotat +plevres +pliages +plias +pliees +plieraient +plies +plinthes +plique +plissames +plisse +plisserais +plisseur +pliure +ploiera +ploieront +plombaginees +plombassent +plombemies +plomberez +plombeuse +plombiferes +plonge +plongeants +plongees +plongerai +plongeront +plots +ployais +ployat +ployions +pluchas +pluchees +plucherez +pluchiez +plumais +plumassent +plumat +plumer +plumerions +plumets +plumiers +plurale +pluralisasse +pluralisee +pluralisme +pluriactifs +pluriannuels +pluriel +plurilingue +plurivoque +plutes +plutonisme +pluviales +pluvinait +pneu +pneumonie +poacee +pochait +pochardant +pochardee +pocharderent +pochardise +pochat +pocherais +pochetee +pochez +pochotheques +podagre +podcastable +podcaster +podcasting +podgoriciens +podie +podologique +podzol +podzolisas +podzolise +podzoliseras +podzolisions +poelage +poelassent +poeler +poeleries +poeliers +poetereau +poetisaient +poetisera +poetiserons +pognais +pognat +pognerais +pognez +pogrome +poignait +poignardais +poignardat +poignardez +poignassions +poigneraient +poignes +poignissent +poilant +poilat +poilerais +poilez +poinconna +poinconnasse +poindrait +poins +pointait +pointat +pointent +pointeriez +pointeuse +pointillais +pointillat +pointilleuse +pointillons +pointues +poireautait +poireautates +poireautons +poirotais +poirotat +poiroteras +poirotions +poisons +poissardes +poisse +poisseras +poisseux +poissonnier +poitrails +poivrage +poivrassent +poivrer +poivrerions +poivrions +polacre +polards +polarisais +polarisasses +polarises +polderiens +polderisees +polderiserez +polders +polemiquait +polemiquates +polemiquons +policames +police +policeraient +polices +policlinique +poliomyelite +polirais +polissable +polisses +polissoires +polissonnas +polissonnera +polissures +politicardes +politiqua +politiquerai +politisait +politisates +politises +pollens +pollinisai +pollinise +polliniseras +pollinisions +polluant +polluat +polluerais +pollueur +polo +polos +polyamine +polyarthrite +polycarpique +polychlorure +polycopiasse +polycopient +polyculture +polyedre +polyethers +polygamie +polyglobulie +polygyne +polyinsature +polymerase +polymeriser +polymorphes +polynevrites +polyphasees +polyphoniste +polyploidie +polyporees +polyptyque +polysemiques +polysulfures +polytherme +polytric +polyurique +pomiculture +pommadas +pommadees +pommaderez +pommai +pommassent +pommees +pommelassent +pommelerent +pommellerais +pomment +pommerent +pommetes +pommons +pomologies +pompages +pompas +pompees +pomperais +pompette +pompiere +pompistes +pomponnasse +pomponnent +pomponneriez +ponant +poncais +poncat +poncer +poncerions +poncho +ponctionnat +ponctionniez +ponctualites +ponctuates +ponctuels +ponctuerez +pond +ponderales +ponderates +ponderees +pondererez +ponderiez +pondimes +pondites +pondre +pondus +pongides +pontages +pontasses +pontenegrin +ponterait +pontets +pontifiai +pontifiasse +pontificat +pontifierait +pontifiiez +ponton +popah +poplite +populaces +popularisa +populariser +poquai +poquassiez +poquerai +poqueront +poracees +porcelets +porches +poreux +pornos +porphyries +porques +porridges +portageaient +portages +portais +portantes +portatif +porterez +porteurs +portiez +portionnes +portoricains +portraituree +portuaire +posada +posassent +posemetre +poserent +poseuses +positionnai +positivames +positive +positiverait +positiviez +positonium +posologiques +possedantes +possede +possederas +possedions +possibilites +postais +postasses +postcolonial +postdatant +postdatee +postdaterent +postdatons +postdoctorat +postent +posterieur +posterisa +posterisera +posteront +postfacant +postfacee +postfacerent +postfacons +postiches +postillon +postposames +postpose +postposeras +postposions +postulames +postuler +postulerions +posturales +potaches +potamocheres +potassa +potassasses +potassera +potasserons +potassique +potele +potencees +potentiel +poteries +potier +potinais +potinat +potineras +potiniere +potiquets +potorous +pottoks +poubelle +poucasse +poucera +poucerons +pouciers +poudrages +poudrasses +poudrera +poudreriez +poudrez +poudroiement +poudroieriez +poudroyait +poudroyates +pouffa +pouffasses +poufferaient +pouffes +pougnait +pougnates +pougnerent +pougnons +pouilleux +poulagas +poularde +poulettes +poulinai +poulinassiez +poulinerais +poulinez +poulotte +pound +poupees +pouponnage +pouponner +poupons +pourchassa +pourchasses +pourfendait +pourfendiez +pourfendit +pourfendrez +pourghere +pourlechant +pourlechee +pourlechons +pourpree +pourraient +pourriels +pourrirait +pourrisse +pourrissons +poursuites +poursuivant +poursuivi +poursuivrait +pourtours +pourvoirait +pourvoit +pourvoyiez +pourvussent +poussahs +poussasses +poussera +pousserons +poussieres +poussines +poussons +poutounait +poutounates +poutounerait +poutouniez +poutrelles +poutsasse +poutsent +poutseriez +poutures +poutzassent +poutzer +poutzerions +pouvait +pouzzolanes +praesidiums +pragoises +prairial +pralinai +pralinassiez +pralinerai +pralineront +prandiale +pratiquai +pratiquasse +pratiquement +pratiquerez +praxie +preadhesions +prealpines +preavis +preaviser +prebendees +precarisera +precatifs +precautions +precedassent +precedent +precederas +precedions +preceptoral +precession +prechant +prechauffage +prechauffera +prechees +precherez +prechez +precipice +precipitas +precipite +precipiteras +precipitions +precisais +precisat +precises +precitees +precocites +precommandai +precompter +preconcevrai +preconcois +preconcus +preconisee +preconisiez +precuirais +precuisaient +precuisions +precuisons +precurseurs +predatrices +predeceder +predecoupas +predecoupent +predefinies +predefinirez +predestinait +predestiniez +predicable +predications +predictif +predilection +prediquas +prediquees +prediquerez +predira +predirions +predises +predisposat +predisposiez +predit +predominance +predominez +preemballe +preempta +preemptasses +preemptera +preempterons +preemptives +preetablis +preexistames +preexistes +prefabriquee +prefacai +prefacassiez +prefacerai +prefaceront +prefectoral +preferai +preferassiez +preferentiel +prefereras +preferions +prefigurera +prefinancais +prefinancez +prefixal +prefixer +prefixerions +preformais +preformat +preformatera +preforment +preformeriez +preformons +pregenitaux +prehominiens +preinscrite +preinscrivez +prejudiciais +prejudicier +prejugeaient +prejuges +prelassait +prelassates +prelasserait +prelassiez +prelatures +prelavas +prelavees +prelaverez +prele +prelevas +prelevees +preleveras +prelevions +preludait +preludates +preluderent +preludons +prematurite +premediquant +premediquees +premeditai +premediter +premenstruel +premilitaire +premonitions +premourants +premunirait +premunissais +premunit +prenante +prendrai +prendront +prennent +prenommas +prenommees +prenommerez +prenoms +prenotasse +prenotee +prenoterent +prenotions +preoblitere +preoccupez +prepaient +prepaierons +preparais +preparat +preparatrice +preparerais +preparez +prepayames +prepaye +prepayeras +prepayions +preponderant +preposant +preposee +preposerent +prepositif +prepotence +prepsychose +prepuce +prerapport +prereglables +prereglasse +prereglent +preregleriez +prereines +preretraite +presageait +presageates +presagerait +presagiez +presbyteraux +presbytie +prescolaires +prescrirez +prescrivions +prescrivons +presenile +presentames +presentateur +presentees +presenterent +preserva +preservasses +preservera +preserverons +presidais +presidat +presidera +presiderons +presidiez +presomptions +presonorisee +pressa +pressantes +presse +pressentent +pressentions +pressentit +presserent +presseuses +pressionnees +pressura +pressurasse +pressurent +pressureriez +pressuriez +pressurisees +presta +prestas +prestation +presteraient +prestes +prestions +presumable +presumassent +presumer +presumerions +presupposat +presupposiez +presurames +presure +presureras +presurions +pretames +pretates +pretendantes +pretendis +pretendrai +pretendront +pretentaine +preter +preterions +preteritasse +preteritent +preterits +pretexta +pretextasses +pretextera +pretexterons +pretirasse +pretirent +pretireriez +pretoires +pretraille +pretranchees +pretures +prevalences +prevalons +prevalussiez +prevariqua +prevariquiez +prevaudriez +prevenances +prevente +preventives +preverbes +previendrons +previns +prevoient +prevoiriez +prevotaux +prevoyante +prevus +priames +priapismes +priee +priere +prieural +prima +primale +primassiez +primaties +primaux +primerait +primes +primeveres +primiparite +primitives +princes +principalats +principes +priorisai +prioriser +prioritaires +prisames +priserait +priseurs +prison +prissions +privaient +privassions +privations +privatisames +privatisez +prive +priveras +privilege +privilegier +pro +probabilite +probation +probiotique +probleme +proceda +procedasses +procedes +procedures +processifs +processives +prochinoise +proclamait +proclamates +proclament +proclameriez +proclises +proconsuls +procrastinas +procrastiner +procreait +procreates +procreatives +procrees +proctologies +procurais +procurat +procuratrice +procurerais +procureur +prodigalite +prodiguaient +prodigues +producteurs +productiques +productrices +produirions +produises +proedres +profanaient +profanee +profanerent +profanons +proferames +profere +profereras +proferions +professas +professees +professerez +professez +profilais +profilat +profilerais +profileur +profils +profitais +profitasses +profiterons +profitons +profus +progeria +proglottis +programmais +programmat +programmeurs +progressa +progresses +prohibais +prohibat +prohiberais +prohibez +proie +projections +projetaient +projeteurs +projettera +projetteront +prolamine +proletariat +proletarisee +proliferai +proliferasse +proliferiez +prolines +prologues +prolongera +prolongerons +promenai +promenassiez +promenerai +promeneront +promenoirs +prometheens +promette +promettons +promettrions +promis +promissiez +promotion +promotionnas +promotrice +promouvons +promouvrons +prompts +promulgua +promulguera +promusse +pronai +pronasses +pronatrices +pronerait +proneurs +pronominales +prononcames +prononce +prononceras +pronostiquee +propagea +propageasses +propagera +propagerons +propanier +propheties +prophetisant +prophetisees +prophetisons +proposait +proposassiez +proposerai +proposeront +propre +proprettes +propulsais +propulsat +propulserais +propulseur +propulsons +prorogatif +prorogeais +prorogeat +prorogerais +prorogez +prosateurs +proscririons +proscrivait +proscrivis +prosecteur +prosodique +prospect +prospecter +prospection +prospectus +prospererai +prospereront +prostate +prosterna +prosternons +prostituames +prostitue +prostitueras +prostituions +prostres +protamines +proteagineux +protectorats +protegeaient +proteges +proteine +proteique +proteomique +proterogyne +protestai +protestants +protestates +protestes +prothese +protocolai +protocoler +protoetoile +protonema +protopteres +prototypes +protoxydes +prout +prouvant +prouvee +prouverent +prouvons +provencale +proveniez +proverbial +proviendrons +provignai +provigner +provinces +provinrent +proviseur +provisionnel +provoc +provoquant +provoquee +provoquerent +provoquons +proximal +pruche +prudentielle +prudhommaux +pruines +prunelees +prunier +prusse +prussique +psalmiste +psalmodiasse +psalmodient +psalmodiques +pschents +pseudomonas +psi +psilotums +psoas +psst +psychiatres +psychogene +psylle +pterosaure +pterygote +ptyaline +puames +puasses +pubertaire +pubien +publiais +publiat +publicistes +publier +publierions +publiphone +publivores +pucant +puccinias +pucelle +pucerent +puches +puddings +puddlas +puddlees +puddlerez +puddlions +pudicite +puent +puericulteur +pueriles +puerperale +pugilistes +puinees +puisait +puisassions +puisent +puiseriez +puisons +puissent +pulicaires +pullulais +pullulat +pullulerai +pulluleront +pulpaire +pulpites +pulsante +pulsassions +pulsatilles +pulsent +pulseriez +pultacees +pulverisent +pulverisons +puna +punaisassent +punaiser +punaiserions +punches +punicacee +puniraient +punis +punisseur +punitif +punt +pupe +pupipare +pureau +purgatifs +purgeaient +purgeassions +purgera +purgerons +purifiaient +purifiassent +purifierai +purifieront +purinais +purinat +purinerais +purinez +puristes +purotin +purpuriques +puseyismes +pussiez +pustuloses +putassant +putassent +putasseriez +putassiez +putier +putrefiable +putrefier +putrescent +putride +putta +puttasses +puttera +putterons +putto +puys +pycnoses +pygmalions +pylones +pyogenes +pyramidai +pyramidasse +pyramidees +pyramiderez +pyramidons +pyreneiste +pyrethrines +pyridoxal +pyrogravai +pyrograverai +pyrogravure +pyrolysai +pyrolyserai +pyrolyseront +pyromanie +pyrophore +pyrosis +pyrotechnies +pyroxyles +pyrrhonisme +pyruvique +pythien +pythonisse +qanouns +qatarien +qing +quadra +quadrangles +quadrature +quadriennal +quadrige +quadrilla +quadrillasse +quadrillent +quadrilobe +quadriparti +quadriplace +quadrique +quadrupedes +quadruplas +quadruplees +quadrupleras +quadruplex +quai +qualifiable +qualifiants +qualifient +qualifieriez +qualitatif +qualiticiens +quantifiee +quantifions +quantites +quarantieme +quarderonnez +quart +quartageas +quartagent +quartagerez +quartaient +quartasse +quartauts +quartent +quarteriez +quartette +quartilage +quartz +quasar +quassiers +quatorze +quatrillion +quebecises +quebracho +quelles +quemandames +quemande +quemanderas +quemandeuse +quenette +querable +quercitrons +querellant +querellee +querellerent +querelleuses +querulentes +questionnat +questionnes +questure +quetait +quetates +queterait +queteurs +quetschier +queusots +queutas +queuter +queuterions +quia +quicks +quiescente +quiets +quillames +quillat +quillerais +quilleur +quillons +quinaires +quincys +quinidines +quinoleiques +quinquas +quinquina +quintil +quintoierais +quintolets +quintoyiez +quintuplant +quintuplee +quintuplons +quiquageons +quiscales +quittames +quittancer +quittasse +quittent +quitteriez +quiz +quotidienne +qwerty +rabachages +rabachasses +rabachent +rabacheriez +rabachiez +rabaissant +rabaissee +rabaisserait +rabaissiez +rabasses +rabattant +rabatteurs +rabattissent +rabattrai +rabattront +rabbiniques +rabibochames +rabiboche +rabibocheras +rabibochions +rabiotais +rabiotat +rabioterais +rabiotez +rablais +rablat +rablerais +rablez +rabonnir +rabonnirions +rabonnisses +rabotages +rabotasses +rabotent +raboteriez +rabotez +raboudinais +raboudinat +raboudinez +rabougrirai +rabougriront +rabougrissez +rabouilleuse +raboutant +raboutee +rabouterent +raboutons +rabrouassent +rabrouements +rabrouerez +rabrouez +racahouts +raccommodai +raccommodera +raccompagna +raccompagnat +raccompagnez +raccordames +raccorde +raccorderais +raccordez +raccourcira +raccoutumee +raccroc +raccrochas +raccrochees +raccrocheras +raccrocheuse +raccusais +raccusat +raccuserais +raccusez +racemique +rachetable +rachetassent +racheter +racheterions +rachialgies +rachitiques +racinages +racinas +racinee +racinerent +raciniennes +racistes +rackettaient +racketters +racks +raclas +raclees +racleras +racleur +raclure +racolant +racolee +racolerent +racoleuses +racontais +racontassiez +raconterai +raconteront +racoon +racornirais +racornissiez +racrapotames +racrapote +racrapoteras +racrapotions +radar +radassions +radera +raderons +radiaires +radians +radiassions +radiatives +radicalisat +radicalises +radicante +radiculalgie +radier +radierions +radin +radinassent +radiner +radineries +radinismes +radiochimie +radiodiffusa +radiodiffuse +radioelement +radiogrammes +radioguidai +radioguidez +radiologie +radiophare +radioreveils +radiosondage +radiques +radjas +radotai +radotassiez +radoterai +radoteront +radoub +radoubas +radoubees +radouberez +radoubs +radoucirait +radoucissais +rafalai +rafalassiez +rafalerais +rafalez +raffermirai +raffermiront +raffinage +raffinassent +raffinement +raffinerent +raffineurs +raffles +raffolais +raffolat +raffoleras +raffolions +raffutames +raffute +raffuteras +raffutions +rafistolai +rafistolerai +raflait +raflates +raflerait +rafliez +rafraichis +rafteur +ragaillardi +rageante +rageates +ragerent +rageusement +ragondin +ragotas +ragoter +ragoterions +ragougnasses +ragrafais +ragrafat +ragraferais +ragrafez +ragreait +ragreates +ragreerait +ragreiez +raguais +raguat +raguerais +raguez +raiders +raidir +raidirions +raidites +raierez +raillai +raillassiez +raillerai +raillerions +railliez +rainait +rainates +rainerait +rainettes +rainurais +rainurat +rainurerais +rainurez +rairait +raisine +raisonnants +raisonnee +raisonnerait +raisonneurs +rajah +rajeunira +rajeunirons +rajeunisse +rajeunites +rajoutant +rajoutee +rajouterent +rajoutons +rajustant +rajustee +rajusterait +rajustiez +ralais +ralasses +ralentie +ralentirent +ralentissant +raleras +raleuse +ralinguant +ralinguee +ralinguerent +ralinguons +rallasse +rallera +rallerons +ralliant +rallides +rallieraient +rallies +rallongera +rallongerons +rallumais +rallumat +rallumerais +rallumez +ramadanesque +ramageant +ramagees +ramagerent +ramai +ramanchames +ramanche +ramancheras +ramancheuse +ramas +ramassas +ramassees +ramasseras +ramasseur +ramassons +ramdams +ramenais +ramenassent +ramendait +ramendates +ramenderait +ramendeurs +ramenent +rameneriez +ramens +rameras +ramerots +rameuta +rameutasses +rameutera +rameuterons +ramie +ramifiames +ramification +ramifierais +ramifiez +ramions +ramollirais +ramollissent +ramona +ramonasse +ramonent +ramoneriez +ramonons +rampantes +rampe +ramperais +rampez +ramules +rancardaient +rancardes +rancescible +ranci +rancirais +rancissaient +rancissiez +ranconna +ranconnasses +ranconnent +ranconneriez +ranconniez +rancuniere +randomisames +randomisez +randonnant +randonnee +randonnerent +randonneuses +rangeai +rangeassiez +ranger +rangerions +ranide +ranimant +ranimations +ranimerait +ranimiez +rapace +rapaillaient +rapailles +rapasse +rapatriage +rapatrierez +rapatronna +rapatronner +rapent +raperchaient +raperches +raperions +rapetassait +rapetassates +rapetassiez +rapetissas +rapetissees +rapetisseras +rapetissions +raphaelique +rapia +rapicolait +rapicolates +rapicolerait +rapicoliez +rapieca +rapiecasse +rapiecement +rapiecerent +rapiecons +rapinais +rapinat +rapinerais +rapineront +raplati +raplatiras +raplatissait +raplatites +raplomber +rapointies +rapointirez +rapointisse +rappa +rappareillee +rappariai +rapparier +rappassent +rappela +rappelasse +rappeler +rappelons +rapperent +rappeuse +rappliquames +rapplique +rappliqueras +rappliquions +rappointit +rappondez +rappondre +rappondus +rapportai +rapporterai +rapporteront +rapportions +rapprends +rappretaient +rappretes +rapprisse +rapprochai +rapprocher +rappropriat +rappropriiez +rappuierez +rappuyait +rappuyates +rappuyons +raptus +raquas +raquees +raquerez +raquetteuse +rarefiable +rarefiassent +rarefier +rarefierions +rarescent +rasade +rasances +rasassions +rasent +raseriez +rasettes +rasiez +rassasiai +rassasiasse +rassasiement +rassasierent +rassasions +rassemblerez +rassemblez +rasserenames +rasserene +rassereneras +rasserenions +rasseyiez +rassierais +rassir +rassirions +rassisse +rassites +rassoirez +rassortiment +rassortirent +rassoul +rassura +rassuras +rassurees +rassurerez +rasta +ratafia +rataplan +ratatinai +ratatinerai +ratatineront +ratatouiller +ratees +ratelant +ratelee +ratelier +ratellerait +ratelures +raterez +ratiboisais +ratiboisat +ratiboisez +ratier +ratifiames +ratification +ratifierais +ratifiez +ratinait +ratinates +ratinerait +ratineuses +ratiocines +rational +rationalisez +rationaux +rationnas +rationnees +rationnera +rationnerons +ratissage +ratissassent +ratisser +ratisserions +ratissoires +ratonnai +ratonnassiez +ratonnerai +ratonneront +ratons +rattachais +rattachat +rattacherai +rattacheront +rattaqua +rattaquasses +rattaquera +rattaquerons +rattrapable +rattrapas +rattrapees +rattraperez +ratura +raturasse +raturent +ratureriez +raubasines +rauchas +rauchees +raucherez +rauchions +raugmentant +raugmentee +raugmentons +rauquassent +rauquer +rauquerions +ravage +ravageassent +ravager +ravagerions +ravagions +ravalasse +ravalement +ravalerent +ravaliez +ravaudait +ravaudates +ravauderait +ravaudeurs +ravenala +raveurs +ravigota +ravigotas +ravigotees +ravigoterez +ravili +raviliras +ravilissait +ravilites +ravinas +ravinees +ravineras +ravinions +raviraient +ravis +ravisassent +raviser +raviserions +ravissait +ravisseur +ravitaillai +ravitaillera +ravites +ravivas +ravivees +raviverez +ravoir +rayant +rayee +rayerait +rayes +rayonnai +rayonnasse +rayonnement +rayonnerent +rayonniez +rayures +razziait +razziates +razzierait +razziiez +reabonnai +reabonner +reabsorbes +reacclimate +reaccoutuma +reaccoutumez +reacteurs +reactions +reactivees +reactiverez +reactivites +reactualisee +readaptai +readapter +readmettons +readmisses +reaffectait +reaffectates +reaffectes +reaffiliames +reaffilie +reaffilieras +reaffiliions +reaffirmasse +reaffirmee +reaffirmons +reagencerez +reagi +reagiras +reagissait +reagites +reajustames +reajuste +reajusterais +reajustez +realesai +realesassiez +realeserai +realeseront +realignaient +realignera +realignerons +realimentais +realisant +realisateurs +realisera +realiserons +realistement +reamenagea +reamenager +reamorcaient +reamorces +reanimais +reanimat +reanimees +reanimerez +reant +reapparurent +reapprenais +reapprendre +reapprenne +reapprissent +reappropriee +reargenter +rearmaient +rearmassions +rearmera +rearmerons +rearrangeat +rearrangerai +reassigna +reassignent +reassort +reassortis +reassurance +reassurates +reassurerait +reassureurs +reattribuais +rebaissaient +rebaisses +rebaptisames +rebaptise +rebaptiseras +rebaptisions +rebatimes +rebatiriez +rebatissent +rebattais +rebattimes +rebattites +rebattriez +rebella +rebellasses +rebellera +rebellerons +rebetiko +rebiffais +rebiffat +rebifferais +rebiffez +rebiquant +rebiquent +rebiqueriez +reblanchie +reblanchisse +reblochons +rebobiner +reboiraient +rebois +reboisassent +reboisements +reboiserez +reboit +rebondimes +rebondiriez +rebonds +rebootas +rebootees +rebooterez +rebord +rebordassent +reborder +reborderions +rebots +rebouchas +rebouchees +reboucherez +rebouilleur +reboutas +reboutees +rebouteras +rebouteuse +reboutonnait +reboutonne +reboutonnons +rebraguettez +rebranchant +rebranchee +rebranchons +rebrodasse +rebrodent +rebroderiez +rebroussai +rebrousser +rebrulaient +rebrules +rebumes +rebut +rebutants +rebutee +rebuterent +rebutons +recacheta +recachetes +recadrages +recadrasses +recadrera +recadrerons +recalai +recalassiez +recalcifiant +recalcifiee +recalcitrant +recalculames +recalcule +recalculeras +recalculions +recaleraient +recales +recalibrant +recalibree +recalibrons +recapitulat +recapitulees +recardames +recarde +recarderas +recardions +recarrelant +recarrelee +recarrellera +recarrelles +recasasse +recasement +recaserent +recasons +recassassent +recasser +recasserions +recausaient +recauserait +recausiez +recavas +recavees +recaverez +receda +recedasses +recedera +recederons +recelaient +recelassions +receleraient +receles +recemment +recensas +recensees +recenseras +recenseuse +recentra +recentrasse +recentrent +recentreriez +recepa +recepasse +recepent +receperiez +recepons +receptionnat +receptrices +recerclas +recerclees +recerclerez +reces +recettes +recevantes +recevons +recevrons +rechampis +rechampissez +rechangeais +rechangeat +rechangerais +rechangez +rechantas +rechantees +rechanterez +rechapa +rechapasse +rechapent +rechaperiez +rechappai +rechapperai +rechapperont +rechargeai +recharger +rechassais +rechassat +rechasserais +rechassez +rechauffera +rechauffons +rechausserez +reche +rechercher +reches +rechignerez +rechutai +rechutassiez +rechuterais +rechutez +recidivant +recidivat +recidiverais +recidivez +recif +recipient +reciproquant +reciproquees +recit +recitante +recitates +recitera +reciterons +reclama +reclamas +reclame +reclameras +reclamions +reclassasse +reclassement +reclasserent +reclassons +reclouassent +reclouer +reclouerions +recluses +recognitives +recoiffer +recois +recolames +recole +recolerais +recolez +recollames +recolle +recollerai +recolleront +recoltable +recoltants +recoltee +recolterent +recolteuses +recombinera +recommandai +recommander +recommencais +recommencez +recompensait +recompense +recompensons +recompiler +recomposera +recomptage +recompter +reconcentre +reconcilia +recondamner +reconduis +reconduisons +reconfirmat +reconfirmiez +reconfortant +recongelas +recongelees +recongelerez +reconnais +reconnaitra +reconnecter +reconnumes +reconquerrai +reconquete +reconquise +reconsiderai +reconsolidez +reconstituee +recontactais +reconvoquee +recopia +recopiasse +recopient +recopieriez +recoquillai +recoquillez +recordais +recordat +recorderais +recordez +recordwomen +recorriger +recouchaient +recouches +recoudrait +recoupage +recoupassent +recoupements +recouperez +recoupions +recouponnera +recourant +recourbant +recourbee +recourberait +recourbiez +recourions +recourriez +recoururent +recousais +recousirent +recousu +recouvrables +recouvrasse +recouvrement +recouvrerent +recouvrions +recouvririez +recouvrit +recrachas +recrachees +recracherez +recre +recreas +recreation +recreerai +recreeront +recrepies +recrepirez +recrepissait +recrepites +recreusasse +recreusent +recreuseriez +recriai +recriassiez +recrierai +recrieront +recriminames +recriminer +recrirai +recriront +recrite +recrivent +recrivisses +recroisais +recroisat +recroiserais +recroisez +recroisses +recroitrait +recrudescent +recrussent +recrutames +recrute +recruterais +recruteur +rectale +rectifiable +rectifier +rectifiions +rectitude +rectoraux +recueillait +recueillie +recuiraient +recuis +recuisimes +recuisites +reculade +reculassent +reculements +reculerez +reculon +reculottasse +reculottent +recumes +recuperant +recuperera +recupererons +recurages +recurasses +recurera +recurerons +recurrente +recursoires +recusant +recusations +recuserait +recusiez +recycla +recyclait +recyclates +recyclerait +recycleurs +redactionnel +redditions +redecollas +redecoller +redecoraient +redecores +redecoupais +redecoupat +redecoupez +redecouvrait +redecouvrira +redefais +redefaites +redeferais +redefinie +redefinirent +redefinition +redefites +redemandasse +redemandent +redemarrage +redemarrer +redemption +redentees +redeployai +redeployez +redeposant +redeposee +redeposerent +redeposons +redescendiez +redessinai +redessinerai +redevait +redevenions +redeviennent +redevinsses +redevraient +redhibitions +rediffuses +redigeaient +redigeraient +rediges +redimant +redimee +redimer +redimerions +redirections +redirigeames +redirigee +redirigeras +redirigions +rediscutai +rediscuterai +rediseurs +redissions +redoives +redondante +redondates +redonderent +redondons +redonnassent +redonner +redonnerions +redoraient +redorassions +redoreraient +redores +redormes +redormirai +redormiront +redorons +redoublante +redoublates +redoubles +redoutables +redoutasses +redoutera +redouterons +redox +redressas +redressees +redresseras +redresseuse +reductases +reduirais +reduisaient +reduisions +reduisons +redutes +redynamisas +redynamisee +ree +reechelonnez +reecoutant +reecoutee +reecouterent +reecoutons +reecririons +reecrivaient +reecrivions +reecrivons +reedifiees +reedifierez +reedita +reeditasses +reeditera +reediterons +reeduqua +reeduquasses +reeduquera +reeduquerons +reelire +reelisant +reellement +reelussent +reembauchait +reembauche +reembauchons +reemetteur +reemettrait +reemis +reemploi +reemployait +reemployates +reemployons +reempruntera +reenchantait +reenchante +reenchantons +reengageasse +reengagerent +reenregistra +reenregistre +reentendais +reentendra +reentendrons +reenvisagee +reequilibra +reequilibrez +reequipant +reequipee +reequiperent +reequipons +reeriez +reescomptee +reessaie +reessayait +reessayates +reessayerait +reessayiez +reetudias +reetudiees +reetudierez +reevalua +reevaluasses +reevaluent +reevalueriez +reexamens +reexaminer +reexecutes +reexpediames +reexpedie +reexpedieras +reexpediions +reexpliquant +reexpliquees +reexportai +reexporter +refaconnai +refaconnerai +refactures +refaisant +refasses +refendait +refendis +refendrai +refendront +referait +referates +referencant +referencee +referenciez +referentiel +refererait +referiez +refermant +refermee +refermentas +refermentee +refermer +refermerions +refila +refilasses +refilera +refilerons +refinancera +refissent +refixait +refixates +refixerait +refixiez +reflechis +reflechisses +reflectifs +refleta +refletasses +refletera +refleterons +refleuries +refleurirez +refleurisse +reflex +reflexions +reflexologie +refluas +refluer +refluerions +refonda +refondasses +refonderait +refondiez +refondit +refondrez +refont +reformai +reformassiez +reformatas +reformatees +reformaterez +reformation +reformera +reformerons +reformings +reformulais +reformulat +reformulez +refouillant +refouillee +refouilliez +refoulas +refoulees +refouleras +refoulez +refourguait +refourguates +refourguiez +refoutent +refoutrait +refoutues +refractant +refractee +refracterent +refractiez +refrangibles +refrenassent +refrenements +refrenerez +refrigera +refrigeras +refrigerez +refroidi +refroidiras +refugiait +refugiates +refugierait +refugiiez +refumas +refumees +refumerez +refus +refusas +refusees +refuserez +refuta +refutant +refutations +refuterait +refutiez +regagnait +regagnates +regagnerait +regagniez +regalages +regalasses +regalement +regalerent +regaliennes +regardai +regardasse +regardent +regarderiez +regardiez +regarnirai +regarniront +regarnissiez +regatames +regate +regaterez +regatiers +regazonnant +regazonnee +regazonnons +regelasse +regelent +regeleriez +regence +regenerames +regeneres +regentait +regentates +regenterait +regentiez +regies +regimbassent +regimbements +regimberez +regimbez +reginglard +regionalisa +regionalisat +regions +regiriez +regissent +registra +registrasse +registree +registrerent +registrons +reglais +reglat +reglementer +reglera +reglerons +regleuses +reglos +regnante +regnates +regnerent +regniez +regommais +regommat +regommerais +regommez +regonflait +regonflates +regonfles +regorgeait +regorgeates +regorgerait +regorgiez +regrattait +regrattates +regratterait +regrattier +regreais +regreat +regreerais +regreez +regreffer +regreons +regresserai +regresseront +regressons +regrettait +regrettates +regretterait +regrettiez +regrimpames +regrimpe +regrimperas +regrimpions +regrossis +regroupant +regroupee +regrouperait +regroupiez +regulait +regulat +regulees +regulerez +regulieres +regurgitait +regurgitates +regurgites +rehabilitat +rehabilites +rehabituames +rehabitue +rehabitueras +rehabituions +rehaussant +rehaussee +rehausserait +rehausseurs +rehydratai +rehydrater +reichstag +reifias +reifie +reifieras +reifiions +reimplantees +reimportai +reimporter +reimposaient +reimposes +reimprimai +reimprimerai +reimputait +reimputates +reimputes +reincarceree +reincarnai +reincarner +reincorpore +reindexa +reindexasses +reindexera +reindexerons +reinettes +reinfecter +reinjectas +reinjectees +reinjecterez +reins +reinscriras +reinscrites +reinscriviez +reinserasse +reinserent +reinsereriez +reinsertions +reinstallent +reinstaurat +reinstauriez +reintegrames +reintegrat +reintegrerai +reintroduite +reinventas +reinventees +reinventerez +reinventons +reinvestites +reinviter +reitera +reiteras +reiteration +reitererai +reitereront +rejaillies +rejaillirez +rejaillisse +rejection +rejetames +rejete +rejetonne +rejetterait +rejoignait +rejoignis +rejoindrai +rejoindront +rejointoya +rejointoyeur +rejouai +rejouassiez +rejouerai +rejoueront +rejouira +rejouirons +rejouites +rejugeas +rejugent +rejugerez +relacai +relacassiez +relacerai +relaceront +relachas +relachees +relacheras +relachions +relaierais +relaissa +relaissasses +relaissera +relaisserons +relancais +relancat +relancerais +relanceur +relapses +relargis +relarguant +relarguee +relarguerent +relarguons +relatassent +relater +relaterions +relativisai +relativisera +relativite +relavames +relave +relaveras +relavions +relaxante +relaxates +relaxeraient +relaxes +relayais +relayat +relayerais +relayeur +relectrice +releguaient +relegues +relevage +relevasse +relevement +releverent +releveuses +reliais +reliat +relierai +relieront +reliftais +reliftat +relifterais +reliftez +reliquats +relirez +relise +reliure +relocalisees +relogea +relogeasses +relogeons +relogeriez +relookages +relookasses +relookera +relookerons +relookings +reloquetas +reloquetees +reloqueterez +relou +relouassent +relouer +relouerions +reluctance +reluirait +reluisaient +reluisez +reluquais +reluquat +reluquerais +reluquez +relussions +remachaient +remaches +remaillais +remaillat +remaillerais +remailleur +remanence +remangeais +remangeat +remangerais +remangez +remaniames +remanie +remanierais +remaniez +remaquillant +remaquillees +remarchai +remarcherais +remarchez +remariait +remariates +remarierait +remariiez +remarquait +remarquates +remarquerait +remarquiez +remasterisas +remasteriser +remastiquat +remastiquiez +remballames +remballe +remballeras +remballions +rembarquasse +rembarquons +rembarrer +rembauches +remblaiera +remblaieront +remblavas +remblavees +remblaverez +remblaya +remblayasse +remblayent +remblayeriez +remblayons +rembobinas +rembobinees +rembobinerez +remboita +remboitasse +remboitement +remboiterent +remboitons +rembougeasse +rembougeons +rembougeriez +rembourrages +rembourrera +rembourrons +remboursames +rembourse +remboursez +rembraierait +rembraya +rembrayasses +rembrayes +rembrunira +rembrunirons +rembrunisses +rembuchais +rembuchat +rembucherai +rembucheront +remediable +remediassent +remedier +remedierions +remelaient +remelassions +remeleraient +remeles +remembrames +remembre +remembrerais +remembrez +rememorant +rememorerait +rememoriez +remercias +remerciees +remercieras +remerciions +remesurant +remesuree +remesurerent +remesurons +remettes +remettras +remeublais +remeublat +remeublerais +remeublez +remimes +remisais +remisat +remiserais +remisez +remisses +remittent +remixais +remixat +remixerais +remixez +remmaillais +remmaillat +remmailleur +remmaillote +remmancha +remmanchera +remmenais +remmenat +remmenerais +remmenez +remmoulait +remmoulates +remmoulerait +remmouliez +remobilisais +remobilisez +remodelait +remodelates +remodelerait +remodeliez +remontaient +remontassent +remonter +remonterions +remontions +remontrance +remontrat +remontrerais +remontrez +remordant +remordisse +remordraient +remords +remorquai +remorquerai +remorqueront +remotiva +remotivasses +remotivent +remotiveriez +remoudra +remoudrons +remouillames +remouille +remouilleras +remouillions +remoulait +remoulates +remoulerait +remouleurs +remoulumes +remous +rempaillas +rempaillees +rempaillerez +rempaillez +rempaquetant +rempaquetees +rempart +rempietasse +rempietement +rempieterent +rempietons +rempilassent +rempiler +rempilerions +remplacables +remplacas +remplacees +remplaceras +remplacions +rempliames +remplie +remplieras +rempliions +remplirent +remplissais +remplissez +remploierai +remploies +remployasse +remployer +remplumais +remplumat +remplumerais +remplumez +rempochant +rempochee +rempocherent +rempochons +remportames +remporte +remporteras +remportions +rempotant +rempotee +rempoterent +rempotons +remprunter +remuable +remuante +remuates +remueraient +remues +remunera +remunerasses +remuneres +renaclait +renaclates +renaclerent +renaclons +renaissants +renaitrai +renaitront +renaquissiez +renardames +renarde +renarderas +renardieres +renaudais +renaudat +renauderas +renaudions +rencaissames +rencaisse +rencaissez +rencardames +rencarde +rencarderas +rencardions +renchainames +renchaine +renchaineras +renchainions +renchausser +rencherimes +rencheririez +rencognas +rencognees +rencognerez +rencontra +rencontrera +rendait +rendions +rendons +rendormie +rendormirait +rendossai +rendosserai +rendosseront +rendrait +rendue +renegocia +renegociasse +renegociee +renegocions +renettai +renettassiez +renetterai +renetteront +renfaitaient +renfaites +renfermames +renferme +renfermerais +renfermez +renfilant +renfilee +renfilerent +renfilons +renflammais +renflammat +renflammez +renflassions +renflera +renflerons +renflouai +renflouer +renfoncaient +renfoncera +renfoncerons +renforcai +renforcerez +renforciez +renforcirent +renforcons +renformirait +renformit +renfrognant +renfrognee +renfrogniez +rengageant +rengagees +rengagerait +rengagiez +rengainasse +rengainent +rengaineriez +rengorgea +rengorgeons +rengorgeriez +rengraissat +rengraissiez +rengrenas +rengrenees +rengreneras +rengrenions +reniasse +reniement +renierent +reniflaient +reniflasses +reniflent +renifleriez +renifleuses +renines +renippasse +renippent +renipperiez +renitences +renom +renommassent +renommer +renommerions +renonca +renoncas +renoncees +renonceras +renonculacee +renotames +renote +renoteras +renoteuse +renouait +renouates +renoueraient +renoues +renouvelai +renouvelasse +renouveler +renouvellera +renouvelles +renovasse +renovations +renoveraient +renoves +renquillames +renquille +renquilleras +renquillions +renseignasse +renseignons +rentables +rentamames +rentame +rentameras +rentamions +rentates +renterait +rentier +rentoilaient +rentoiles +rentra +rentraierait +rentrais +rentrantes +rentraya +rentrayasse +rentrayent +rentrayeriez +rentrayiez +rentreraient +rentres +renumerotee +renverra +renverront +renversante +renversates +renverses +renvidais +renvidat +renviderais +renvideur +renvoient +renvoyas +renvoyees +renvoyions +reoccupasse +reoccupee +reoccuperent +reoccupons +reoperasse +reoperent +reopereriez +reorchestrai +reordonnant +reordonnee +reordonnons +reorganise +reorganisons +reorientees +reorienterez +reouvert +reouvre +reouvris +reoxygenai +reoxygenerai +repaieraient +repaira +repairasses +repaires +repaisse +repaitraient +repand +repandimes +repandites +repandriez +reparable +reparaissent +reparaitrais +reparant +reparateurs +reparent +repareriez +reparlaient +reparles +repartageai +repartages +repartements +repartir +repartirions +repartisses +repartitions +reparusse +repassa +repassasse +repassent +repasseriez +repassiez +repavames +repave +repaverais +repavez +repayant +repayee +repayerent +repayons +repechas +repechees +repecherez +repeigna +repeignasses +repeignera +repeignerons +repeindrais +repeint +repenchant +repenchee +repencherent +repenchons +rependiez +rependit +rependrez +repens +repensassent +repenser +repenserions +repentais +repentes +repentions +repentiriez +reperaient +reperassions +repercames +reperce +reperceras +repercions +repercutait +repercutates +repercutiez +reperdes +reperdissiez +reperdras +reperdues +repererais +reperez +repertoriait +repertorie +repertorions +repesassent +repeser +repeserions +repetaient +repetassions +repeteraient +repetes +repetition +repetitrices +repetrirais +repetrissons +repeuplas +repeuplees +repeupleras +repeuplions +repipant +repipee +repiperent +repipons +repiquas +repiquees +repiquerez +repiquions +replacant +replacee +replacerait +replaciez +replantas +replante +replanteras +replantions +replatrames +replatre +replatreras +replatrions +repletive +replia +repliasse +replications +repliera +replierons +repliquais +repliquat +repliquerais +repliquez +replissames +replisse +replisseras +replissions +reploierait +replongeai +replongerai +replongeront +reployames +reploye +replu +repointasse +repointent +repointeriez +repolie +repolirent +repolissais +repolit +reponde +repondions +repondons +repondrions +reponses +reportait +reportates +reporterait +reporteur +reposa +reposas +reposees +reposerez +repositionne +repoudrais +repoudrat +repoudrerais +repoudrez +repourvoit +repourvue +repourvut +repoussant +repoussat +repousserai +repousseront +reprecises +reprenais +reprendre +repreneuses +representes +repressifs +reprimames +reprimandera +reprimasses +reprimera +reprimerons +repris +reprisas +reprisees +repriserez +reprisions +reprobateurs +reprochais +reprochat +reprocherais +reprochez +reproduction +reproduirait +reproduisais +reproduite +reprofilames +reprofile +reprofileras +reprofilions +reprogrammes +reprouvas +reprouvees +reprouverez +reps +republia +republiasses +republie +republieras +republiions +repudiant +repudiations +repudierait +repudiiez +repugnames +repugnasses +repugnera +repugnerons +repulsions +repussions +reputasse +reputee +reputerent +reputons +requalifier +requerais +requerons +requerrons +requetasse +requetent +requeteriez +requiere +requinquais +requinquat +requinquez +requisition +requissions +requittas +requittees +requitterez +rerouta +reroutasse +reroutent +rerouteriez +resalai +resalassiez +resalerai +resaleront +resalira +resalirons +resalissez +resarceles +rescapassent +rescaper +rescaperions +rescindables +rescindas +rescindees +rescinderez +rescision +reseau +reseautas +reseautees +reseauterez +reseaux +resemais +resemat +resemerais +resemez +resequant +resequee +resequerent +resequons +reservas +reservation +reserverais +reservez +residaient +residas +residences +residera +residerons +residuel +resignais +resignat +resigner +resignerions +resiliables +resiliasses +resilience +resilierais +resiliez +resinait +resinates +resinerait +resineuses +resinifere +resistaient +resistas +resister +resisterions +resistions +resituaient +resitues +resolubles +resolussiez +resolutoire +resolvants +resonant +resonnaient +resonnas +resonnees +resonneras +resonnions +resorbant +resorbee +resorberent +resorbons +resoudrais +resout +respectais +respectat +respecterais +respectez +respectueuse +respirais +respirasses +respirerait +respiriez +resquilles +ressacs +ressaieriez +ressaignames +ressaigne +ressaigneras +ressaignions +ressaisirais +ressassait +ressassates +ressasserait +ressasseurs +ressauta +ressautasses +ressautera +ressauterons +ressayages +ressayasses +ressayera +ressayerons +ressemais +ressemat +ressemblant +ressemblat +ressembleras +ressemblions +ressemelais +ressemelat +ressemelions +ressemerais +ressemez +ressente +ressentimes +ressentirez +resserrait +resserrates +resserres +resservant +resservions +resserviriez +resservit +ressortent +ressortira +ressortirons +ressortisse +ressorts +ressouder +ressourcera +ressouvenais +ressouvenue +ressouvinsse +ressuai +ressuassiez +ressuerai +ressueront +ressuierait +ressuis +ressurgirais +ressuscitas +ressuscitent +ressuyages +ressuyasses +ressuyes +restames +restasses +restaurait +restaurat +restaurees +restaurerez +restauroute +resterai +resteront +restituais +restituat +restituerais +restituez +restoroute +restreignez +restreignit +restreindrez +restrictif +restructura +restructurat +restylames +restyle +restyleras +restylions +resultants +resumames +resume +resumeras +resumions +resurgent +resurgirai +resurgiront +resurgissiez +retabli +retabliras +retablissait +retablissons +retaillas +retaillees +retaillerez +retais +retamames +retame +retameras +retamez +retapait +retapates +retaperait +retapiez +retapissasse +retapissent +retard +retardants +retardates +retardent +retarderiez +retassure +retatasse +retatent +retateriez +retaxai +retaxassiez +retaxerai +retaxeront +reteigne +reteindrais +reteint +retelephoner +retend +retendimes +retendites +retendre +retendus +retentait +retentates +retenterait +retenteurs +retentira +retentirons +retentisse +retentites +retercages +retercasses +retercera +retercerons +retersai +retersassiez +reterserai +reterseront +reticent +reticulait +reticulates +reticules +reticuloses +retiendriez +retifs +retigeasse +retigera +retigerons +retinien +retinoiques +retinssiez +retirages +retirasse +retiree +retirerait +retiriez +retissames +retisse +retisseras +retissions +retombaient +retombassent +retombements +retomberez +retond +retondimes +retondites +retondriez +retoquage +retoquassent +retoquer +retoquerions +retordages +retordeur +retordisse +retordra +retordrons +retorquai +retorquerai +retorqueront +retorsions +retouchames +retouche +retoucheras +retoucheuse +retoupait +retoupates +retouperait +retoupiez +retournait +retournates +retournes +retracait +retracates +retracerait +retraciez +retractais +retractat +retracterai +retracteront +retractif +retractons +retraduirez +retraduise +retraie +retrairez +retraitais +retraitasses +retraitent +retraiteriez +retrancha +retranchent +retranscrira +retransmisse +retravaille +retraversa +retraverses +retrayantes +retreci +retreciras +retrecissait +retrecissons +retreignez +retreindre +retreints +retremper +retribuaient +retribues +retroactes +retroagi +retroagirez +retroagisse +retrocedai +retrocederai +retroflechi +retrograda +retrograder +retroussais +retroussat +retrousserai +retrouvai +retrouver +retroviral +retuba +retubasses +retubera +retuberons +reuilly +reunifiames +reunifierais +reunifiez +reuniras +reunissaient +reunissiez +reussira +reussirons +reussissez +reutilisai +reutiliser +revaccinai +revacciner +revaille +revalez +revalusses +revanchait +revanchasses +revanchera +revancherons +revas +revassaient +revasserait +revasses +revates +revaudrons +revecumes +revee +reveillas +reveillees +reveillerez +reveillez +revelaient +revelassions +revelee +revelerent +revelons +revendais +revendez +revendiquant +revendiquees +revendis +revendrai +revendront +revenons +reverai +reverassiez +reverberant +reverberat +reverbererai +reverchait +reverchates +revercherait +reverchiez +reverdirai +reverdiront +reverdoirs +reverer +revererions +reverifies +revernir +revernirions +revernisse +reveront +reverrons +reversales +reversates +reverserai +reverseront +reversions +revetais +revetimes +revetirez +revetissions +reveuillent +reveut +reviendriez +revif +revigorantes +revigoration +revigorerais +revigorez +revinsses +reviraient +revirassions +revirera +revirerons +revisables +revisasses +revisera +reviserons +revisionnel +revisitai +revisiterai +revisiteront +revissai +revissassiez +revisserai +revisseront +revitalisais +revitaliser +revivait +revivifiai +revivifier +revivrait +revocable +revoila +revolant +revolee +revolerent +revolons +revoltants +revoltee +revolterent +revoltons +revolverisas +revolverises +revoquaient +revoques +revotames +revote +revoteras +revotions +revoudrions +revouloir +revoulusses +revoyiez +revulsai +revulsasse +revulsent +revulseriez +revulsions +rewritames +rewrite +rewriteras +rewriteurs +rewritrices +rhabillames +rhabille +rhabillerais +rhabilleur +rhapsode +rheiformes +rheobases +rheophiles +rhesus +rhetique +rhingraviat +rhinolophe +rhizobiums +rhizophore +rhizotomes +rhodesien +rhodiait +rhodiates +rhodiera +rhodierons +rhodites +rhodophycees +rhomboedres +rhonalpine +rhubarbes +rhumasse +rhumatisants +rhumatologie +rhumbs +rhumerait +rhumes +rhynchonelle +rhytine +riait +ribat +ribesiee +riblant +riblee +riblerent +riblon +ribose +ribot +riboulames +riboules +ricaines +ricanantes +ricane +ricaneras +ricaneuse +ricercare +richelieux +richissimes +ricochas +ricocher +ricocherions +ricotta +ridames +ride +rider +riderions +ridiculisat +ridiculises +ridule +riens +rif +riffle +riflames +riflat +riflerais +riflette +rifts +rigidifies +rigoise +rigolait +rigolasses +rigoleraient +rigoles +rigolo +rigottes +rikikis +rimailla +rimaillasses +rimaillera +rimaillerons +rimaillons +rimassions +rimee +rimerent +rimeuses +rinca +rincasse +rincee +rincerent +rinceurs +ring +ringardant +ringardee +ringarderent +ringardisa +ringardiser +ringgits +riotai +riotassiez +rioterais +riotez +ripaient +ripaillerai +ripailleront +ripais +ripat +riper +riperions +ripienos +ripolinait +ripolinates +ripolinerait +ripoliniez +ripostames +riposte +riposteras +ripostions +ripuaires +rirent +risberme +risible +risquait +risquates +risquerait +risquiez +rissolai +rissolassiez +rissolerai +rissoleront +ristournait +ristournates +ristourniez +ritales +ritualisera +ritualistes +rivaient +rivalisait +rivalisates +rivaliserent +rivalisons +rivassions +river +riverais +rivesaltes +rivetant +rivetee +rivetons +rivetterez +rivez +rivure +riyal +riziculteurs +roadsters +robais +robat +roberai +roberont +robin +robinsons +robotisant +robotiserait +robotisiez +robustas +rocaillages +rocambole +rochages +rochasses +rochee +rocherais +roches +rochions +rockeurs +rocouai +rocouassiez +rocouerai +rocoueront +roda +rodaillames +rodaille +rodaillerez +rodais +rodat +roderai +roderont +rodoirs +roentgenium +rogatons +rognas +rognees +rognerez +rogneux +rognonnai +rognonnerais +rognonnez +roguee +roidi +roidiras +roidissait +roidites +roillassent +roiller +roillerions +roitelets +roller +rollot +romaines +romancames +romance +romanceras +romancez +romand +romanesque +romanisaient +romanisees +romaniserez +romanismes +romantiques +romantiser +romarin +rompait +rompis +romprai +rompront +romsteck +ronchonna +ronchonnent +ronchonniez +roncieres +rondel +rondeur +rondir +rondirions +rondisses +ABANDONNIQUES +ABASOURDIRIEZ +ABREVIATRICES +ABRUTISSANTES +ABRUTISSEUSES +ABSENTASSIONS +ABYSSINIENNES +ACADEMICIENNE +ACAGNARDAIENT +ACCASTILLAGES +ACCASTILLERAI +ACCELERERIONS +ACCESSOIRISER +ACCESSOIRISTE +ACCIDENTERAIS +ACCOMPLISSIEZ +ACCOUCHASSIEZ +ACCOUCHERIONS +ACCOURCISSAIT +ACCOUTUMERAIS +ACCREDITERONT +ACCROCHASSENT +ACCROCHEMENTS +ACCUEILLERONT +ACCUEILLIRENT +ACCULTURERENT +ACCUMULASSENT +ACHROMATISERA +ACOUSTICIENNE +ACQUIESCAIENT +ACTINOMYCOSES +ACTIONNASSIEZ +ACTIONNERIONS +ACTUALISERONS +ACUPUNCTRICES +ADDITIONNEURS +ADJECTIVAIENT +ADMONESTERONT +ADVERBIALISAT +AEROSTATIQUES +AFFAIBLISSANT +AFFAIRERAIENT +AFFAISSASSIEZ +AFFAISSERIONS +AFFECTIONNONS +AFFERMERAIENT +AFFLEURASSIEZ +AFFLEURERIONS +AFFLIGERAIENT +AFFOUAGERIONS +AFFOUILLASSES +AFFOUILLERIEZ +AFFOURCHERAIS +AFFOURRAGEREZ +AFFRANCHIRENT +AFFRIANDERONT +AFRICANISATES +AFRICANTHROPE +AGENOUILLERAI +AGGLOMERERONT +AGGLUTINERENT +AGGRAVASSIONS +AGREMENTERIEZ +AGROBIOLOGIES +AGROCHIMIQUES +AGROPASTORALE +AGROTOURISMES +AHEURTERAIENT +AIGUILLETIONS +AIGUILLONNANT +AIGUILLONNENT +ALCALINISERAI +ALCOOLOMANIES +ALGORITHMIQUE +ALIMENTASSIEZ +ALLAITERAIENT +ALLERGOLOGIES +ALLITERATIONS +ALLONGEASSIEZ +ALLUVIONNAMES +ALLUVIONNERAS +ALLUVIONNIONS +ALOURDIRAIENT +ALPHABETISAIT +ALPHABETISIEZ +AMARYLLIDACEE +AMBIANCERIONS +AMBITIONNAMES +AMBITIONNEREZ +AMELIORATEURS +AMENUISASSIEZ +AMENUISERIONS +AMERICANISIEZ +AMINCISSAIENT +AMOINDRISSANT +AMONCELASSENT +AMORTISSABLES +AMOURACHAIENT +ANACARDIACEES +ANACHRONIQUES +ANASTOMOSERAI +ANENCEPHALIES +ANESTHESIQUES +ANGIOCHOLITES +ANGOISSASSIEZ +ANGUSTIFOLIEE +ANIMADVERSION +ANIMALISERAIS +ANNIHILATIONS +ANNONCIATIONS +ANOVULATOIRES +ANTHROPISERAI +ANTHROPOPHILE +ANTIACNEIQUES +ANTIBIOTIQUES +ANTICIPERIONS +ANTIDEMARRAGE +ANTIDETONANTE +ANTIGIVRANTES +ANTILIBERALES +ANTIPARALLELE +ANTIREFLEXIFS +ANTISISMIQUES +ANTITHERMIQUE +APLACENTAIRES +APOPHANTIQUES +APOPLECTIQUES +APPAREILLEREZ +APPAUVRISSIEZ +APPENDISSIONS +APPERTISAIENT +APPERTISERONS +APPESANTIRIEZ +APPESANTISSES +APPLAUDISSANT +APPLAUDISSIEZ +APPOINTASSIEZ +APPOINTIRIONS +APPRECIATRICE +APPRIVOISERAI +APPROFONDIRAI +APPROPRIERENT +APPROVISIONNA +APPROXIMATION +APRAGMATISMES +AQUARELLERENT +AQUITANIENNES +ARABISASSIONS +ARCBOUTASSENT +ARCBOUTERIONS +ARCHIMEDIENNE +ARCHITECTONIE +ARGUMENTATEUR +ARITHMOGRAPHE +AROMATHERAPIE +ARRONDISSAGES +ARTIFICIALISE +ARTIFICIALITE +ARYTENOIDIENS +ASCENSIONNAIS +ASCENSIONNIEZ +ASOMATOGNOSIE +ASPERGEASSIEZ +ASPHALTASSENT +ASPHALTERIONS +ASPHYXIASSENT +ASPHYXIERIONS +ASSAILLISSENT +ASSAISONNASSE +ASSASSINASSES +ASSECHERAIENT +ASSERMENTEREZ +ASSERVISSANTE +ASSERVISSEUSE +ASSIGNASSIONS +ASSIMILATEURS +ASSOMBRISSIEZ +ASSOMMASSIONS +ASSOMMERAIENT +ASSOUPIRAIENT +ASSOUPLISSAIS +ASSOUPLISSEUR +ASSOURDISSONS +ASSOUVIRAIENT +ASTREIGNISSES +ASTROMETRISTE +ASTROPHYSIQUE +ASYNCHRONISME +ATHEROMATEUSE +ATTARDASSIONS +ATTARDERAIENT +ATTEINDRAIENT +AURIFICATIONS +AUTHENTIQUERA +AUTOADHESIVES +AUTOANALYSAIS +AUTOCASSABLES +AUTOCENSURAIT +AUTOCENSUREES +AUTODETRUISEZ +AUTODISSOLVES +AUTODISSOUDRE +AUTOEVALUAMES +AUTOEVALUERAS +AUTOEVALUIONS +AUTOFINANCONS +AUTOFLAGELLES +AUTOGRAPHIIEZ +AUTOPRODUIRAI +AUTOPROMOTION +AUTOPROTOLYSE +AUTOSATISFAIT +AVALISASSIONS +AVALISERAIENT +AVANTAGEASSES +AVERTISSEMENT +AVILISSEMENTS +AVIRONNASSENT +AVOCASSASSIEZ +BACTERIOLYSES +BADIGEONNIONS +BAFOUILLERAIT +BAGARRASSIONS +BAGARRERAIENT +BAILLONNERONT +BAISOTASSIONS +BAISOTERAIENT +BAMBOUSERAIES +BANALISATIONS +BANCARISATION +BANCARISERAIS +BANDERILLEROS +BANQUETASSIEZ +BARAQUASSIONS +BARBARISASSES +BARBARISERONS +BARBOUILLIONS +BARGUIGNERONS +BAROMETRIQUES +BARRICADERAIS +BASCULASSIONS +BATAILLASSIEZ +BATHYMETRIQUE +BEAUVAISIENNE +BECHEVETERIEZ +BECQUETASSIEZ +BEHAVIORISTES +BELLETTRIENNE +BELLIFONTAINS +BELLINZONAISE +BEMOLISASSENT +BEMOLISERIONS +BENEFICIAIENT +BENEFICIASSES +BERRIASIENNES +BIBELOTASSIEZ +BIBERONNERIEZ +BICAMERALISME +BICULTURELLES +BIENVEILLANTS +BIGARREAUTIER +BIGOPHONERENT +BILATERALISME +BILOQUASSIONS +BILOQUERAIENT +BIOCARBURANTS +BIOCONVERSION +BIOGENETIQUES +BIPASSASSIONS +BIPASSERAIENT +BIQUOTIDIENNE +BISTOURNERONT +BIVOUAQUERAIS +BLACKBOULAGES +BLACKBOULERAI +BLACKLISTAMES +BLACKLISTEREZ +BLANCHISSANTE +BLASONNASSENT +BLASONNERIONS +BLATERERAIENT +BONIMENTERENT +BONIMENTEUSES +BORDELISERENT +BORDURASSIONS +BORDURERAIENT +BOUBOULASSENT +BOUCHARDASSES +BOUCHARDERONS +BOUCHONNERAIS +BOUFFONNAIENT +BOUFFONNERAIS +BOUFFONNERONT +BOUILLIRAIENT +BOUILLONNANTE +BOULEVERSASSE +BOURDONNAIENT +BOURDONNIERES +BOURGEONNATES +BOURGEONNERAS +BOURGEONNIONS +BOURRELASSIEZ +BOURSOUFFLAGE +BOURSOUFLERAI +BOUSILLASSENT +BOUSILLERIONS +BOYCOTTASSENT +BOYCOTTERIONS +BRACHIOSAURES +BRANDEVINIERS +BREDOUILLANTE +BREDOUILLEURS +BRESILLASSIEZ +BRETAILLERENT +BRETTELLERONT +BRILLANTERAIT +BRILLANTINERA +BRINGUASSIONS +BRINGUEBALANT +BRINQUEBALENT +BRONCHASSIONS +BRONCHERAIENT +BRONCHOSPASME +BROUILLASSAIT +BROUILLERIONS +BRUISSASSIONS +BRUISSERAIENT +BRUNCHASSIONS +BRUSQUERAIENT +BRUTALISERENT +BUDGETASSIONS +BUDGETERAIENT +BUDGETISERAIT +BUISSONNAIENT +BUISSONNERONT +BUNKERISASSES +BUNKERISERONS +BUREAUCRATISA +CABOTINASSIEZ +CAFARDASSIONS +CAFARDERAIENT +CAFOUILLERONS +CAILLASSERONT +CAILLOUTASSES +CAILLOUTERONS +CALCULABILITE +CALORIFUGEAIS +CALORIFUGEREZ +CALORISATIONS +CALOTTASSIONS +CALOTTERAIENT +CAMBOULASSENT +CAMBOULERIONS +CAMBRIOLERONT +CAMPHRASSIONS +CAMPHRERAIENT +CAMPIGNIENNES +CANADIANISENT +CANCELLASSIEZ +CANCERISATION +CANCERISERAIS +CANONISASSIEZ +CANONISERIONS +CANTONALISANT +CANTONALISENT +CANTONALISTES +CAOUTCHOUTENT +CAPARACONNENT +CAPITONNERIEZ +CAPITULATIONS +CAPORALISERAI +CAPPARIDACEES +CAQUETTERIONS +CARACTERISERA +CARAMBOLERAIS +CARAMBOUILLER +CARAMELISERAI +CARAVAGESQUES +CARBONATAIENT +CARBONATERONS +CARBONITRUREZ +CARCAILLASSES +CARDIOGRAPHES +CARPICULTURES +CARRELASSIONS +CARTELLISASSE +CARTOUCHERAIS +CARTOUCHERONT +CASEMATASSIEZ +CATALEPTIQUES +CATAPULTERAIS +CATECHISERAIT +CATECHOLAMINE +CATEGORICITES +CATEGORISASSE +CATHOLICISENT +CAUCHEMARDAIS +CAUCHEMARDENT +CAUCHEMARDONS +CENTRALISEREZ +CENTRIFUGERAS +CENTRIFUGEUSE +CEPHALOCORDES +CERIFICATEURS +CERTIFIASSIEZ +CERTIFIERIONS +CESALPINIACEE +CESARISASSENT +CESARISERIONS +CHAMAILLASSES +CHAMAILLERIEZ +CHAMBRASSIONS +CHAMPLEVERIEZ +CHANCELLERIEZ +CHANFREINASSE +CHANSONNASSES +CHANSONNERONS +CHANTONNASSES +CHANTONNERIEZ +CHANTOURNASSE +CHAPEAUTERIEZ +CHARBONNERIES +CHARDONNASSES +CHARDONNERETS +CHARNELLEMENT +CHARPENTAIENT +CHARPENTERONS +CHARRIOTERONT +CHARROYASSIEZ +CHARRUASSIONS +CHARRUERAIENT +CHATAIGNERAIT +CHATOUILLERAS +CHAUDRONNIERE +CHEMINASSIONS +CHENOPODIACEE +CHEVALERAIENT +CHEVAUCHEMENT +CHEVAUCHERENT +CHEVREFEUILLE +CHIFFONNAIENT +CHIFFONNERONS +CHIFFRERAIENT +CHIROGRAPHIES +CHIROPRACTIES +CHLORACETIQUE +CHLOROPHYLLES +CHONDRICHTYEN +CHONDROBLASTE +CHOREGRAPHIAI +CHOREGRAPHIEE +CHOUANNASSIEZ +CHOURINASSENT +CHOURINERIONS +CHRONIQUERAIT +CHRONOMETRERA +CICATRISABLES +CIRCONCISSENT +CIRCONSCRIREZ +CIRCONSCRIVIS +CIRCONSTANCIE +CIRCULARISAIS +CIRCULERAIENT +CISAILLASSIEZ +CISAILLERIONS +CLACTONIENNES +CLAIRONNERAIS +CLAIRSEMERAIT +CLAQUETASSIEZ +CLASSIFIASSES +CLASSIFIERAIT +CLAUDIQUERIEZ +CLAUSTRATIONS +CLAVETTERIONS +CLAVICULAIRES +CLERICALISENT +CLIGNOTEMENTS +CLIMATISERAIT +CLIQUETTERAIT +CLOCHARDISERA +CLOISONNAIENT +CLOISONNERONS +CLOTURASSIONS +CLOTURERAIENT +COADJUTORERIE +COALESCERIONS +COALISASSIONS +COALISERAIENT +COBALTHERAPIE +CODETENTRICES +CODIRIGEASSES +COGENERATIONS +COGNITICIENNE +COGNITIVISMES +COHABITASSIEZ +COINCIDASSENT +COLIBACILLOSE +COLLABORERIEZ +COLLECTIONNER +COLLECTIVISEZ +COLLETAILLAIS +COLLIGEASSENT +COLLOQUASSENT +COLLOQUERIONS +COLMATASSIONS +COLMATERAIENT +COLONISATIONS +COLPOPLASTIES +COMBIENTIEMES +COMMENTERIONS +COMMISSIONNAI +COMMISSIONNEZ +COMMUNICATION +COMMUNIERIONS +COMMUTERAIENT +COMPARAISSANT +COMPARAITRONT +COMPARUSSIONS +COMPATIBILITE +COMPENETRATES +COMPETIRAIENT +COMPETISSIONS +COMPETITRICES +COMPLAISANTES +COMPLEMENTERA +COMPOGRAVEURS +COMPOSTASSIEZ +COMPRESSAIENT +COMPROMETTANT +COMPROMETTONS +COMPROMISSIEZ +COMPTABILISAI +COMPTABILISEE +COMPULSATIONS +CONCENTRERONS +CONCEPTUALISE +CONCERTASSIEZ +CONCERTERIONS +CONCHYLICOLES +CONCILIABULES +CONCRETERIONS +CONCRETISASSE +CONCUPISCENTS +CONCURRENCENT +CONCURRENTIEL +CONDENSATEURS +CONDESCENDENT +CONDITIONNANT +CONFECTIONNES +CONFEDERERONT +CONFERENCIERS +CONFESSIONNAL +CONFIGURERENT +CONFIRMATIONS +CONFITURERIES +CONFORTASSENT +CONFORTERIONS +CONGELERAIENT +CONGESTIONNEZ +CONGLOMERASSE +CONGLUTINANTS +CONGLUTINERAI +CONGRATULERAI +CONGREGANISME +CONJECTUREREZ +CONJOIGNAIENT +CONJOINDRIONS +CONNAISSABLES +CONSCIENCIEUX +CONSCIENTISAS +CONSCRIPTIONS +CONSEILLERAIS +CONSEILLERONT +CONSEQUEMMENT +CONSERVATRICE +CONSIDERATION +CONSIDERERONS +CONSONANTIQUE +CONSONASSIONS +CONSPIRASSENT +CONSPIRERIONS +CONSPUASSIONS +CONSPUERAIENT +CONSTELLATION +CONSTELLERAIS +CONSTERNERONT +CONSTIPASSIEZ +CONSTIPERIONS +CONSTITUAIENT +CONSTRICTIONS +CONSTRUCTEURS +CONSULTERIONS +CONTACTASSIEZ +CONTACTOLOGUE +CONTEMPLERENT +CONTENEURISEZ +CONTENTASSENT +CONTENTEMENTS +CONTESTATEURS +CONTEXTUALISE +CONTINUERIONS +CONTOURNEMENT +CONTOURNERENT +CONTRACTERIEZ +CONTRACTURAIS +CONTRAIGNIONS +CONTRAINDRONS +CONTRARIASSES +CONTRARIERONS +CONTRAROTATIF +CONTREBANDIER +CONTREBASSONS +CONTREBRAQUEZ +CONTRECARRIEZ +CONTREFAISONS +CONTREFICHIEZ +CONTREMANDERA +CONTREPLAQUEZ +CONTRETYPATES +CONTRETYPERAS +CONTRETYPIONS +CONTREVENTIEZ +CONTRIBUERIEZ +CONTRIBUTEURS +CONTROUVERAIT +CONTROVERSENT +CONVAINQUISSE +CONVALESCENTE +CONVENTUALITE +CONVERSATIONS +CONVIVIALITES +CONVOLVULACEE +CONVULSASSENT +CONVULSERIONS +CONVULSIONNAI +CONVULSIONNEZ +COORGANISAMES +COORGANISEREZ +COPARTAGEASSE +COPENHAGOISES +COPERMUTERAIT +COPRESIDASSES +CORDONNASSIEZ +CORDONNERIONS +CORRESPONDANT +COSIGNERAIENT +COSMOGRAPHIES +COSTARICAINES +COUILLONNASSE +COUPAILLERENT +COUPASSASSIEZ +COUPELLASSENT +COUPEROSAIENT +COURBATTURERA +COURROUCERENT +COUTURASSIONS +COUTURERAIENT +COVOITURERAIS +CRACHOUILLIEZ +CRAMPONNASSES +CRAMPONNERIEZ +CRAQUETASSENT +CRAYONNASSENT +CRAYONNERIONS +CRESSICULTURE +CRETINISAIENT +CRIMINALISERA +CRISTALLERIES +CRISTALLINIEN +CRISTALLISAIT +CRISTOLIENNES +CROQUIGNOLETS +CROUPIONNAMES +CROUSTILLEUSE +CRUCIFIASSIEZ +CRUCIFIERIONS +CRYOCHIRURGIE +CRYOGENISAMES +CRYOGENISERAS +CRYOGENISIONS +CRYOSCOPIQUES +CUMULOSTRATUS +CURETTERAIENT +CUSTOMISERAIT +CYANOSASSIONS +CYANOSERAIENT +CYANURERAIENT +CYCLOSPORTIVE +CYCLOTOURISME +CYPRINIFORMES +CYSTOGRAPHIES +CYTOSQUELETTE +DAMASQUINERIE +DANDINASSIONS +DANSOTASSIONS +DANSOTTERIONS +DEBADGEASSENT +DEBAGOULERONT +DEBAILLONNERA +DEBALOURDAMES +DEBALOURDEREZ +DEBAPTISERAIT +DEBAUCHASSENT +DEBAUCHERIONS +DEBENZOLERENT +DEBILLARDATES +DEBILLARDERAS +DEBILLARDIONS +DEBLAIERAIENT +DEBOBINASSIEZ +DEBOGUASSIONS +DEBOGUERAIENT +DEBOSSELLERAI +DEBOURRASSENT +DEBOURREMENTS +DEBOUSSOLAMES +DEBOUSSOLEREZ +DEBRAGUETTAIS +DEBRANCHASSES +DEBRANCHERIEZ +DEBREAKASSIEZ +DEBRIDASSIONS +DEBROCHASSIEZ +DEBROUILLAMES +DEBROUSSAILLE +DEBROUSSERONT +DEBUCHASSIONS +DEBUCHERAIENT +DEBUDGETISAIT +DEBUSQUASSENT +DEBUSQUEMENTS +DEBUTANISATES +DEBUTANISERAS +DEBUTANISIONS +DECADENASSAIS +DECALAMINAMES +DECALAMINEREZ +DECALCIFIASSE +DECALOTTERAIT +DECANILLERIEZ +DECAPELLERAIT +DECAPSULERENT +DECARBONATAIS +DECARBOXYLASE +DECARCASSAMES +DECARCASSEREZ +DECARCERATION +DECENTRERIONS +DECEREBRERENT +DECERVELASSES +DECERVELLERAS +DECHAUSSERONT +DECHIFFONNANT +DECHIFFRASSES +DECHIFFRERIEZ +DECHIQUETAMES +DECHOQUASSIEZ +DECINTRASSIEZ +DECINTRERIONS +DECLASSIFIAIS +DECLAVETERENT +DECLENCHEMENT +DECLENCHERENT +DECLENCHEUSES +DECLINATOIRES +DECLINERAIENT +DECLINQUERAIS +DECLIQUETATES +DECLOISONNAIS +DECOIFFASSENT +DECOIFFEMENTS +DECOLLERAIENT +DECOLORASSENT +DECOMMETTRONT +DECOMMISSIONS +DECOMPENSEREZ +DECOMPLEXERAI +DECOMPOSERAIS +DECOMPRESSION +DECONCENTREES +DECONGELASSES +DECONGELERIEZ +DECONNECTATES +DECONNECTERAS +DECONNECTIONS +DECONSEILLAIS +DECONSIGNASSE +DECONSTIPATES +DECONSTIPERAS +DECONSTIPIONS +DECONSTRUIREZ +DECONTAMINEES +DECONTRACTENT +DECORTIQUAMES +DECORTIQUEREZ +DECORTIQUEUSE +DECOUPLASSENT +DECOUPLERIONS +DECOURAGEATES +DECOUVRIRIONS +DECREDITERENT +DECREMENTASSE +DECREPITAIENT +DECREPITERONS +DECROCHASSIEZ +DECROCHERIONS +DECULASSERIEZ +DECUSCUTEUSES +DEDIFFERENCIA +DEDOMMAGERAIS +DEDRAMATISAIT +DEFALQUASSIEZ +DEFATIGUERAIS +DEFAUFILERENT +DEFAUSSASSENT +DEFAUSSERIONS +DEFAVORISAMES +DEFAVORISEREZ +DEFENDISSIONS +DEFERRASSIONS +DEFICELASSIEZ +DEFIGEASSIONS +DEFIGURATIONS +DEFISCALISAIT +DEFLAGRASSIEZ +DEFLAGRERIONS +DEFLATASSIONS +DEFLATERAIENT +DEFONCERAIENT +DEFORCASSIONS +DEFORCERAIENT +DEFORESTERONT +DEFRAGMENTEES +DEFRANCHIRONT +DEFRIPASSIONS +DEFRIPERAIENT +DEFROISSAIENT +DEFRUITASSIEZ +DEGALONNERAIT +DEGANTASSIONS +DEGANTERAIENT +DEGASOLINASSE +DEGAUCHISSAIS +DEGAUCHISSIEZ +DEGAZOLINAMES +DEGAZOLINEREZ +DEGAZONNEMENT +DEGAZONNERENT +DEGENERASSENT +DEGLINGUERENT +DEGOBILLERAIT +DEGOULINERONT +DEGOURBIFIIEZ +DEGRAISSERAIS +DEGRAVOIEMENT +DEGRAVOIERIEZ +DEGROSSASSENT +DEGROSSERIONS +DEGROSSISSANT +DEGROSSISSONS +DEGROUPASSENT +DEGROUPEMENTS +DEGUEULASSAIS +DEGUEULASSONS +DEGUILLASSIEZ +DEGUISERAIENT +DEGURGITATION +DEGURGITERAIS +DEHARNACHAMES +DEHARNACHEREZ +DEHOUILLERIEZ +DEHOUSSASSENT +DEHOUSSERIONS +DEJANTASSIONS +DEJANTERAIENT +DEJAUGERAIENT +DELABIALISANT +DELABIALISENT +DELABYRINTHER +DELARDERAIENT +DELASSASSIONS +DELEGITIMEREZ +DELIBERATIVES +DELIGNIFIASSE +DELIMITATIONS +DELINEAMENTES +DELINEARISEES +DELISTASSIONS +DELISTERAIENT +DELIVRASSIONS +DELIVRERAIENT +DELPHINARIUMS +DEMAGNETISERA +DEMAIGRISSIEZ +DEMATERIALISE +DEMAZOUTAIENT +DEMERITERIONS +DEMIELLASSENT +DEMIELLERIONS +DEMILITARISAI +DEMILITARISEE +DEMINERALISES +DEMOCRATISIEZ +DEMODULASSENT +DEMONSTRATIFS +DEMONTRASSIEZ +DEMOTIVASSENT +DEMOULASSIONS +DEMOULERAIENT +DEMOUSTIQUONS +DEMULTIPLIAIS +DEMUSELLERAIT +DEMUTISASSIEZ +DEMUTISERIONS +DEMYELINISONS +DEMYSTIFIANTS +DENEIGEASSIEZ +DENICOTINISEZ +DENITRATATION +DENITRERAIENT +DENITRIFIANTE +DENITRIFIERAS +DENITRIFIIONS +DENSIFICATION +DENTUROLOGIES +DENUCLEARISES +DEPACSASSIONS +DEPACSERAIENT +DEPALISSERAIT +DEPAQUETAIENT +DEPAQUETTEREZ +DEPARTAGERONT +DEPASSIONNAIT +DEPASSIONNEES +DEPHOSPHATONS +DEPIGMENTATES +DEPOETISAIENT +DEPOITRAILLAS +DEPOLITISEREZ +DEPOLYMERISAS +DEPOUDRASSIEZ +DEPOUILLAIENT +DEPOUILLERONS +DEPRECIATIONS +DEPRENDRAIENT +DEPROGRAMMAIS +DEPROLETARISA +DEPROTEGERAIS +DEPUCELLERONS +DEQUALIFIERAS +DEQUALIFIIONS +DEREALISERENT +DEREFERENCAIT +DEREFERENCEES +DERISOIREMENT +DERMATOGLYPHE +DERMATOMYCOSE +DERUPITASSIEZ +DESACCENTUAIS +DESACCLIMATEZ +DESACCORDAMES +DESACCORDEREZ +DESACIDIFIONS +DESACRALISONS +DESADAPTASSES +DESADAPTERIEZ +DESAFFILIATES +DESAGRAFERENT +DESAGREGEATES +DESAGREGERAIT +DESAJUSTASSES +DESAJUSTERIEZ +DESALIGNAIENT +DESALIGNERONS +DESALINISATES +DESALTERERIEZ +DESAMBIGUISAS +DESAMIDONNAIT +DESAMIDONNEES +DESAMINASSENT +DESAMORCERENT +DESANGOISSERA +DESAPPARIAMES +DESAPPARIEREZ +DESAPPOINTIEZ +DESAPPROUVANT +DESARRIMASSES +DESARRIMERONS +DESASSEMBLANT +DESASSIMILAIT +DESASSORTIRAS +DESATELLISEES +DESATOMISERAI +DESECHOUASSES +DESECHOUERONS +DESECTORISONS +DESEMPARERIEZ +DESEMPESERONT +DESEMPLIRIONS +DESENCHAINIEZ +DESENCOLLAMES +DESENCOLLEREZ +DESENCOMBRIEZ +DESENCRASSERA +DESENERVERAIT +DESENFLAMMAIT +DESENFLAMMEES +DESENFOURNIEZ +DESENGAGERENT +DESENGLUASSES +DESENGLUERONS +DESENGRENAMES +DESENGRENEREZ +DESENIVRASSES +DESENIVRERONS +DESENLACERAIS +DESENLAIDIRAI +DESENRAIERIEZ +DESENSABLASSE +DESENSIMAIENT +DESENSORCELEZ +DESENTOILAGES +DESENTOILERAI +DESENTRAVASSE +DESENVASERAIS +DESENVELOPPAS +DESENVOUTASSE +DESEPAISSIREZ +DESEQUILIBRAI +DESEQUILIBRES +DESERTISATION +DESESPEREMENT +DESEXCITAIENT +DESEXCITERONS +DESEXUALISAIS +DESHABILLASSE +DESHABITUATES +DESHABITUERAS +DESHABITUIONS +DESHONORANTES +DESHUMIDIFIAI +DESHUMIDIFIEE +DESHYDRATANTE +DESIGNERAIENT +DESILLUSIONNA +DESINCARCERAI +DESINCARCEREE +DESINCARNEREZ +DESINCORPOREZ +DESINCRUSTANT +DESINDEXASSES +DESINDEXERIEZ +DESINFECTAMES +DESINFECTEURS +DESINHIBAIENT +DESINSCRIRAIT +DESINSCRIVONS +DESINSECTISES +DESINSTALLANT +DESINSTALLENT +DESINTEGRATES +DESINTERESSAS +DESINTOXIQUES +DESOBLIGERENT +DESOCIALISERA +DESODORISASSE +DESOLIDARISAS +DESORBITERAIT +DESOSSERAIENT +DESOXYGENATES +DESSAOULAIENT +DESSERTISSAIT +DESSILLASSIEZ +DESSOUCHASSES +DESSOUCHERONS +DESSUINTERAIT +DESTABILISANT +DESTABILISONS +DESTINATAIRES +DESTITUASSENT +DESTITUERIONS +DESTRUCTIVITE +DESTRUCTURANT +DESULFITERAIS +DESULFURERAIT +DESURCHAUFFER +DETALONNERENT +DETAPISSERIEZ +DETEIGNISSIEZ +DETERIORERONT +DETERMINAIENT +DETERMINATIVE +DETERMINERAIS +DETHEINASSENT +DETHEINERIONS +DETIENDRAIENT +DETORDISSIONS +DETOXIQUERIEZ +DETRACTASSIEZ +DETRAQUASSENT +DETRAQUEMENTS +DETROUSSEMENT +DETROUSSERENT +DETROUSSEUSES +DEVALUASSIONS +DEVASTERAIENT +DEVELOPPERAIT +DEVERNISSAGES +DEVERNISSIONS +DEVERROUILLER +DEVIRGINISAIT +DEVIRGINISEES +DEVISAGERIONS +DEVITRIFIAMES +DIAGONALISONS +DIALOGUASSIEZ +DIAPHRAGMEREZ +DIFFERENTIENT +DIFFRACTAIENT +DIGITALISEREZ +DIGITALISIONS +DILACERASSIEZ +DILACERERIONS +DILIGENTERONT +DISCOMPTERAIT +DISCONTINUIEZ +DISCONVINSSES +DISCOUNTERONT +DISCRIMINASSE +DISCUTAILLAIS +DISCUTAILLENT +DISCUTAILLIEZ +DISGRACIASSES +DISGRACIERONS +DISJONCTERIEZ +DISPARAISSAIS +DISPATCHERENT +DISPENSERIONS +DISPROPORTION +DISQUALIFIONS +DISSIMILITUDE +DISSIPATRICES +DISSOLUSSIONS +DISSONASSIONS +DISSUADASSIEZ +DISTENDRAIENT +DISTILLASSIEZ +DISTINGUAIENT +DISTORDISSIEZ +DISTRAIRAIENT +DISTRIBUERONT +DIVERGEASSIEZ +DIVERSIFIASSE +DIVINISATIONS +DOCUMENTAIRES +DOCUMENTERENT +DOGMATISERONT +DOLLARISATION +DOLLARISERAIS +DOMESTIQUATES +DOMESTIQUERAS +DOMESTIQUIONS +DOMICILIERONS +DOUILLASSIONS +DOUILLERAIENT +DRAGEIFIERONT +DRAMATISERENT +DRASTIQUEMENT +DROITISASSIEZ +DROITISERIONS +DRYOPITHEQUES +DUALISERAIENT +DUDGEONNAIENT +DUODECIMAINES +DYNAMITASSENT +DYSGENESIQUES +DYSKINETIQUES +DYSTROPHIQUES +EBOUILLANTAGE +EBOUILLANTIEZ +EBOURGEONNIEZ +EBOURIFFASSES +EBOURIFFERONS +EBRANLASSIONS +EBRECHASSIONS +EBULLIOSCOPIE +ECARQUILLAMES +ECARQUILLEREZ +ECARTELASSIEZ +ECARTELERIONS +ECCLESIOLOGIE +ECHALASSAIENT +ECHALASSERONS +ECHANTILLONNA +ECHARDONNATES +ECHARDONNERAS +ECHARDONNIONS +ECHAROGNERONT +ECHELONNAIENT +ECHELONNERONS +ECHENILLASSES +ECHENILLERONS +ECHEVELASSENT +ECHOGRAPHIONS +ECLAIRCISSAIS +ECOEURASSIONS +ECONOMISERONT +ECORNIFLERENT +ECORNIFLEUSES +ECOURTICHATES +ECOURTICHERAS +ECOURTICHIONS +ECOUVILLONNER +ECRIVAILLERAS +ECRIVAILLEUSE +ECRIVASSERIEZ +ECTOPARASITES +ECUSSONNAIENT +EDITIONNERONT +EFFAROUCHATES +EFFECTUASSIEZ +EFFONDRASSIEZ +EFFONDRERIONS +EGRATIGNERIEZ +EJACULASSIONS +EJACULATRICES +ELECTRIFIERAS +ELECTRIFIIONS +ELECTRISASSES +ELECTRISERIEZ +ELECTROCUTENT +ELISABETHAINS +ELLIPSOGRAPHE +EMAILLASSIONS +EMAILLERAIENT +EMBARBOUILLAS +EMBARDOUFLANT +EMBARRASSAMES +EMBASTILLEREZ +EMBLAVASSIONS +EMBOBINASSIEZ +EMBOUQUASSENT +EMBOUQUEMENTS +EMBOURRASSENT +EMBOURRERIONS +EMBOUTEILLAGE +EMBOUTISSOIRS +EMBRAIERAIENT +EMBRANCHASSES +EMBRANCHERIEZ +EMBRAQUASSIEZ +EMBRASERAIENT +EMBREVERAIENT +EMBRIGADERAIS +EMBRINGUERENT +EMBRONCHERAIT +EMBROUILLAMES +EMBROUILLATES +EMBRYOGENIQUE +EMBUGNASSIONS +EMBUGNERAIENT +EMMAGASINEREZ +EMMAILLOTATES +EMMENAGEAIENT +EMMITONNERIEZ +EMPAQUETEUSES +EMPATTASSIONS +EMPECHASSIONS +EMPETRERAIENT +EMPOCHASSIONS +EMPOCHERAIENT +EMPOISONNERAS +EMPOISONNEUSE +EMPOISSONNERA +EMPOUSSIERIEZ +EMPRESSASSENT +EMPRESSEMENTS +EMPRESURERIEZ +EMPRUNTASSIEZ +EMPUANTISSAIS +EMPUANTISSONS +EMULSIFIANTES +ENCAGOULERAIS +ENCAISSASSENT +ENCAISSEMENTS +ENCANAILLATES +ENCAPSULAIENT +ENCAPSULERONS +ENCASTELASSES +ENCASTELERONS +ENCASTRASSENT +ENCASTREMENTS +ENCELLULEMENT +ENCELLULERENT +ENCHANTASSENT +ENCHANTEMENTS +ENCHANTERESSE +ENCHAPERONNAS +ENCHATELERONT +ENCHEMISERIEZ +ENCHERISSEUSE +ENCHIFRENEREZ +ENCLIQUETAGES +ENCOURAGERENT +ENDETTERAIENT +ENDOLORIRIONS +ENDOLORISSENT +ENDOMMAGEATES +ENFARGERAIENT +ENFIROUAPATES +ENFIROUAPERAS +ENFIROUAPIONS +ENFOURCHERAIT +ENFREIGNAIENT +ENFREINDRIONS +ENFUTAILLERAI +ENGONCASSIONS +ENGOUFFREMENT +ENGOUFFRERENT +ENGOURDIRIONS +ENGOURDISSENT +ENGRANGERIONS +ENHARNACHAMES +ENHARNACHEREZ +ENJAMBASSIONS +ENJAVELASSENT +ENJAVELLERAIS +ENJUPONNAIENT +ENLIASSASSENT +ENLIASSERIONS +ENLUMINASSIEZ +ENQUIQUINANTE +ENQUIQUINEURS +ENREGIMENTONS +ENREGISTREREZ +ENROUILLAIENT +ENROULASSIONS +ENRUBANNERENT +ENSAUVAGEAMES +ENSAUVAGERAIT +ENSEMENCEMENT +ENSEMENCERENT +ENSEVELISSAIT +ENSOLEILLERAI +ENTABLASSIONS +ENTENEBRAIENT +ENTERORENALES +ENTHOUSIASMAT +ENTOMOLOGISTE +ENTORTILLAGES +ENTOURLOUPAIT +ENTOURLOUPEES +ENTRACCUSAMES +ENTRAIDASSIEZ +ENTRAINASSENT +ENTRAINEMENTS +ENTREDEVORANT +ENTREMANGIONS +ENTREMETTEURS +ENTREMETTRAIS +ENTREPOSERAIT +ENTREPRENDRAI +ENTREPRISSENT +ENTREREGARDEE +ENTRETAILLANT +ENTRETIENDREZ +ENTRETOISASSE +ENTRETUERIONS +ENTROUVRIRONS +ENUCLEERAIENT +ENVIRONNERONT +ENVISAGEABLES +ENVISAGEASSES +EPAISSISSEURS +EPINCELLERIEZ +EPIPHENOMENES +EPISTEMOLOGIE +EPOINTERAIENT +EPONTILLASSES +EPONTILLERONS +EPOULARDERAIT +EPOUSSETTERAI +EPOUSTOUFLERA +EPOUVANTERAIS +EQUILIBRERAIT +EQUIVOQUERONT +ERAILLERAIENT +ERGOTHERAPIES +EROTOLOGIQUES +ERYTHROPOIESE +ESCAGASSERIEZ +ESCALADASSIEZ +ESCARGOTIERES +ESCARRIFIERAS +ESCARRIFIIONS +ESCLAVAGERONT +ESCOFFIASSIEZ +ESCOMPTASSENT +ESCOMPTERIONS +ESGOURDASSENT +ESGOURDERIONS +ESPOUTIRAIENT +ESPOUTISSIONS +ESQUINTASSENT +ESQUINTERIONS +ESTAMPILLAGES +ESTAMPILLERAI +ESTERIFIERONT +ESTHETISASSES +ESTHETISERIEZ +ESTOMAQUERENT +ESTOQUASSIONS +ESTOQUERAIENT +ESTOURBISSENT +ESTRAPADASSES +ESTRAPADERONS +ESTRAPASSATES +ESTRAPASSERAS +ESTRAPASSIONS +ETALINGUERAIT +ETALONNASSIEZ +ETALONNERIONS +ETANCHERAIENT +ETHNICISATION +ETHNICISERAIS +ETRONCONNATES +ETRONCONNERAS +ETRONCONNIONS +EUPHORBIACEES +EUPHORISASSES +EUPHORISERIEZ +EUROSTRATEGIE +EVANGELISEREZ +EVEILLASSIONS +EVEILLERAIENT +EVENTRERAIENT +EXANTHEMATEUX +EXCLAMASSIONS +EXCREMENTIELS +EXCURSIONNONS +EXFILTRATIONS +EXFOLIERAIENT +EXORCISASSIEZ +EXORCISERIONS +EXOSQUELETTES +EXPATRIASSENT +EXPECTORERENT +EXPERIMENTIEZ +EXPERTISERAIS +EXPLICITERAIS +EXPLOITASSIEZ +EXPLOITERIONS +EXPLOSASSIONS +EXPLOSERAIENT +EXPORTATRICES +EXPURGEASSIEZ +EXTEMPORANEES +EXTERMINAIENT +EXTERMINERENT +EXTEROCEPTEUR +EXTOURNASSIEZ +EXTRAPOLATION +EXTRAPOLERAIS +EXTRAVAGANCES +EXTRAVAGUAMES +EXTRAVASAIENT +EXTRAVASERONS +FABRIQUASSIEZ +FAINEANTERENT +FAISANDASSIEZ +FALSIFICATION +FAMILIARISEES +FANGOTHERAPIE +FARANDOLERIEZ +FARFOUILLIONS +FASCINERAIENT +FASCISASSIONS +FASEILLASSENT +FELICITASSENT +FEMINISASSIEZ +FEMINISERIONS +FERMENTATIVES +FERRALLITIQUE +FERROSILICIUM +FERRUGINEUSES +FETICHISATION +FETICHISERAIS +FEUILLETTEREZ +FIABILISERAIS +FILIALISERENT +FILIGRANERONT +FILOCHASSIONS +FILOCHERAIENT +FINANCIARISEZ +FISSIONNAIENT +FLAMBOYASSENT +FLANCHERAIENT +FLATTEUSEMENT +FLEMMARDERONT +FLEURETASSIEZ +FLINGUASSIONS +FLINGUERAIENT +FLUIDIFIERENT +FLUXIONNAIRES +FOISONNASSENT +FOISONNERIONS +FONCTIONNERAS +FONCTIONNIONS +FORLONGEAIENT +FORMALISATION +FORMALISERAIS +FOUDROYASSENT +FOUETTERAIENT +FOURCHASSIONS +FOURCHERAIENT +FOURGONNERONT +FOURMILLERIEZ +FOURVOYASSENT +FOUTIMASSERAS +FOUTIMASSIONS +FRACTIONNERAI +FRAGILISERONT +FRAMBOISAIENT +FRAMBOISERAIE +FRAMBOISERONS +FRANCHISAIENT +FRANCONIENNES +FREQUENTABLES +FREQUENTASSES +FREQUENTERAIT +FRETILLASSENT +FRETILLERIONS +FRICASSASSIEZ +FRICTIONNEREZ +FRISTOUILLIEZ +FROEBELIENNES +FROISSERAIENT +FROUFROUTASSE +FUMERONNASSES +FUSIONNERIONS +FUTUROLOGIQUE +GABIONNASSENT +GABIONNERIONS +GADGETISERIEZ +GALACTOSEMIES +GALVANISERONT +GANGRENASSENT +GANGRENERIONS +GARGOUILLAMES +GARGOUILLEREZ +GARROTTASSENT +GARROTTERIONS +GASTRECTOMIES +GASTROPLASTIE +GAZOUILLAIENT +GEMMOLOGISTES +GENERATIONNEL +GEOCENTRISMES +GEOGRAPHIQUES +GEOMARKETINGS +GEOTHERMIQUES +GERMANISERONT +GERMANOPHOBIE +GERONTOCRATIE +GERONTOLOGUES +GIBBERELLINES +GLANDOUILLANT +GLAVIOTASSENT +GLAVIOTERIONS +GLOBICEPHALES +GLORIFIASSENT +GLYCERINERONT +GOBICHONNERAI +GODILLERAIENT +GOINFRASSIONS +GOINFRERAIENT +GONOCHORISMES +GOUDRONNERIES +GOURMANDERAIS +GOURNABLERONT +GRAFIGNASSENT +GRAFIGNERIONS +GRAILLASSIONS +GRAILLONNATES +GRAILLONNEREZ +GRAISSASSIONS +GRAISSERAIENT +GRANDISSEMENT +GRANITASSIONS +GRANITERAIENT +GRAPPILLERONT +GRATICULAIENT +GRATICULERONS +GRAVELLERIONS +GRAVILLONNERA +GRENELASSIONS +GRIFFONNERAIS +GRIGNOTASSENT +GRIGNOTEMENTS +GRILLAGERIONS +GRINCHASSIONS +GRISAILLERAIS +GROGNASSERIEZ +GROGNONNASSES +GROSSISSEMENT +GROUILLASSIEZ +GROUILLERIONS +GUEULETONNONS +GUILLEMETAMES +GUILLOCHASSES +GUILLOCHERONS +GUILLOTINEURS +GUINCHASSIONS +GYNOGENETIQUE +HABITUERAIENT +HAILLONNEUSES +HAMECONNAIENT +HANDICAPERIEZ +HANNETONNASSE +HARMONICORDES +HARMONISASSES +HARMONISERIEZ +HECTOMETRIQUE +HELIOTROPISME +HELITREUILLAT +HELLENISASSES +HELLENISERIEZ +HEMIPARASITES +HEMIPTEROIDES +HEMORROIDALES +HEPTATHLONIEN +HERBAGEASSIEZ +HERBORISERONS +HEROICOMIQUES +HETEROGREFFES +HETEROPLASTIE +HETEROZYGOTIE +HIERARCHISEES +HIPPIATRIQUES +HIPPOTECHNIES +HISPANISERENT +HISTORIASSIEZ +HOLOGRAPHIIEZ +HOMOGENEIFIER +HOMOLOGUERAIS +HORIZONTALITE +HORTILLONNAGE +HUMANISASSIEZ +HUMANISERIONS +HUMIDIFIAIENT +HUMIDIFIERIEZ +HYDROCRAQUEUR +HYDROCUTERIEZ +HYDROFUGERONT +HYDROGENERONT +HYDROLYSERONS +HYDROPULSEURS +HYPERGOLIQUES +HYPERNATREMIE +HYPERTHERMIES +HYPNOTHERAPIE +HYPNOTISERENT +HYPNOTISEUSES +HYPOCHLORITES +HYPOTHEQUATES +HYPOTHEQUERAS +HYPOTHEQUIONS +HYSTERIFORMES +ICHNEUMONIDES +ICHTYOLOGIQUE +ICONOCLASTIES +IDEALISERIONS +IDIOSYNCRASIE +IDIOTIFIASSES +IDIOTIFIERONS +ILLEGITIMITES +ILLUSIONNERAI +IMMATRICULAIS +IMMORTALISEES +IMMUNISASSIEZ +IMMUNISERIONS +IMPARTAGEABLE +IMPERSONNELLE +IMPERTURBABLE +IMPLANTATIONS +IMPORTATRICES +IMPORTUNERAIT +IMPRECATOIRES +IMPREVOYANCES +IMPRIMABILITE +IMPROBATRICES +IMPROVISASSES +IMPROVISERAIT +INACCORDABLES +INACTIVASSIEZ +INACTIVERIONS +INAPPLICABLES +INASSIMILABLE +INAUGURATIONS +INCANTATOIRES +INCINERASSIEZ +INCONCEVABLES +INCONSISTANTS +INCORPORALITE +INCORPORASSES +INCORRECTIONS +INCRUSTASSIEZ +INCRUSTERIONS +INDEFRICHABLE +INDEMONTRABLE +INDIANISERONT +INDIFFERASSES +INDIFFERENCES +INDIFFERERIEZ +INDIRECTEMENT +INDISPOSAIENT +INDIVIDUALISA +INDULGENCIANT +INEXPLICABLES +INEXPUGNABLES +INFANTILISEES +INFATUASSIONS +INFERENTIELLE +INFERIORISEES +INFIBULATIONS +INFORMATISIEZ +INGURGITERENT +INHOSPITALITE +INITIALASSENT +INITIALISERAI +INOPPORTUNITE +INQUALIFIABLE +INSAISISSABLE +INSATISFAITES +INSECURISATES +INSECURISERAS +INSECURISIONS +INSIGNIFIANTS +INSINUASSIONS +INSTALLATIONS +INSTILLERIONS +INSTITUTRICES +INSTRUISISSES +INSUBORDONNEE +INSUFFLASSENT +INSUFFLATRICE +INSURPASSABLE +INTAILLASSENT +INTAILLERIONS +INTELLIGENTES +INTENTIONNEES +INTERAFRICAIN +INTERAGISSONS +INTERASTRALES +INTERCEDERONS +INTERCEPTEURS +INTERESSERAIT +INTERFACERONT +INTERFERASSES +INTERFERENTES +INTERIORISONS +INTERJETTERAS +INTERLOQUASSE +INTERMARIAGES +INTERMITTENTS +INTEROCEPTIFS +INTERPELLERAI +INTERPENETRAT +INTERPENETREZ +INTERPOLERAIS +INTERPOSERENT +INTERPRETASSE +INTERROGERIEZ +INTERROMPIONS +INTERROMPRONS +INTERRUPTRICE +INTERTRIBALES +INTERVERTIRAS +INTERVIENDRAI +INTERVIENNENT +INTERVIEWAMES +INTERVIEWEREZ +INTERVINSSENT +INTIMIDASSIEZ +INTRODUCTIONS +INUTILISATION +INVALIDATIONS +INVECTIVERENT +INVESTIRAIENT +IRREDENTISMES +IRREPROCHABLE +ISLAMOLOGIQUE +ISOCHRONIQUES +ISOMERISASSES +ISOMERISERIEZ +ISOTHERMIQUES +ITALIANISATES +JAILLISSEMENT +JARGONNASSENT +JAVELLISERONS +JOINTOYASSENT +JOURNALISATES +JOURNALISERAS +JOURNALISIONS +JUDICIARISANT +JUDICIARISENT +JUPONNASSIONS +JUPONNERAIENT +JUSTIFICATEUR +JUXTAPOSABLES +JUXTAPOSASSES +JUXTAPOSERONS +KAOLINISERONT +KHARTOUMAISES +KITESURFEUSES +LABELLISAIENT +LABELLISERONS +LABIALISERONT +LABIOPALATALE +LABYRINTHITES +LAICISASSIONS +LAMBRISSASSES +LAMBRISSERONS +LAPIDIFIERAIS +LARYNGECTOMIE +LATIFUNDISTES +LATINISASSENT +LECHOUILLERAI +LEMMATISATION +LEMMATISERAIS +LEPIDOSAURIEN +LESIONNERIONS +LEUCOCYTAIRES +LIBERALISASSE +LIGATURASSENT +LIGATURERIONS +LILLIPUTIENNE +LIMOGEASSIONS +LINEARISATION +LIPSCHITZIENS +LONGITUDINALE +LOQUETASSIONS +LUBRIFICATEUR +LUBRIFIERIONS +LUSITANIENNES +LYCOPODIACEES +MAASTRICHTIEN +MACADAMISASSE +MACHURASSIONS +MACHURERAIENT +MACROCYTAIRES +MACROGLOSSIES +MADERISASSENT +MAGASINASSENT +MAGASINERIONS +MAGDALENIENNE +MAGNETISERAIT +MAGNETOMOTEUR +MAGNETOSCOPES +MAINMORTABLES +MAITRISASSIEZ +MALCHANCEUSES +MALENDURANTES +MALVEILLANTES +MAMMALOGIQUES +MANCHONNASSES +MANCHONNERONS +MANGEOTASSENT +MANGEOTERIONS +MANGEOTTAIENT +MANIFESTERAIT +MANIGANCERAIT +MANUCURASSIEZ +MAQUEREAUTAIS +MAQUERELLERAI +MARABOUTERAIS +MARAUDASSIONS +MARAUDERAIENT +MARCHANDERIEZ +MARCHANDISANT +MARCHANDISENT +MARCHANDISONS +MARGUILLIERES +MARIVAUDAIENT +MARIVAUDERAIT +MARMONNASSIEZ +MARMONNERIONS +MARMORISERENT +MAROQUINASSES +MAROQUINERIEZ +MARQUETTERAIT +MARRONNASSENT +MARTENSITIQUE +MARTYRISASSES +MARTYRISERONS +MASSICOTERONT +MASSIFICATION +MATERIALISANT +MATERIALISENT +MATERIALISTES +MATHEMATISANT +MATHEMATISENT +MATRICULERAIS +MAUGREERAIENT +MECONNAITRONS +MEDAILLASSENT +MEDAILLERIONS +MEDIATISERAIT +MEDICALISAMES +MEDICALISERAS +MEDICALISIONS +MEDICAMENTERA +MEGACEPHALIES +MEGATONNIQUES +MELANGEASSIEZ +MELANODERMIES +MENDIGOTERONT +MENTALISERONT +MENTIONNAIENT +MERCANTILISME +MERCERISERENT +MEROVINGIENNE +MESALLIASSIEZ +MESESTIMAIENT +MESESTIMERONS +METALLISERAIT +METALLOIDIQUE +METALLURGISTE +METAPHORISAIS +METASTASAIENT +METATARSIENNE +METEORISERENT +METHACRYLIQUE +METHANISERAIT +MICASCHISTEUX +MICROALVEOLES +MICROCHIMIQUE +MICROFICHAGES +MICROFICHERAI +MICROHISTOIRE +MICROMETRIQUE +MICRONESIENNE +MICRONISATION +MICRONISERAIS +MICROSCOPIQUE +MILLESIMERIEZ +MILLIMETRATES +MILLIMETRERAS +MILLIMETRIONS +MILLIONNAIRES +MINIATURISANT +MINIATURISENT +MINIMALISAMES +MINIMALISERAS +MINIMALISIONS +MISSIONNAIRES +MITIGEASSIONS +MITRAILLAIENT +MIXTIONNAIENT +MODERNISAIENT +MODERNISERENT +MOISSONNASSES +MOISSONNERONS +MOLLUSCICIDES +MONNAYASSIONS +MONNAYERAIENT +MONOLITHISMES +MONOLOGUERONT +MONOPARENTAUX +MONOPHONIQUES +MONTALBANAISE +MONTREUILLOIS +MORPHOGENESES +MORPHOSYNTAXE +MORTIFICATION +MOTORISASSIEZ +MOTORISERIONS +MOUCHARDERIEZ +MOUCHERONNAIT +MOUFETERAIENT +MOULINASSIONS +MOULINERAIENT +MOUVEMENTASSE +MUCILAGINEUSE +MUGUETTERIONS +MULTINATIONAL +MULTIPLEXATES +MULTIPLEXERAS +MULTIPLIERONT +MULTISOUPAPES +MUNICIPALISAT +MUNITIONNASSE +MUTUALISATION +MUTUALISERAIS +MYCODERMIQUES +MYRISTICACEES +MYTHOMANIAQUE +NANOMATERIAUX +NANOPHYSIQUES +NARCOLEPTIQUE +NATURALISAMES +NATURALISERAS +NATURALISIONS +NAUFRAGEAIENT +NAZIFIERAIENT +NECESSITERAIS +NECTARINIIDES +NEGATIONNISME +NEGLIGEASSENT +NEGOCIERAIENT +NEOCOMMUNISTE +NEOLOGISERENT +NEPHROSTOMIES +NEUROBIOLOGIE +NEUROCHIMIQUE +NEUROTOXIQUES +NIVOGLACIAIRE +NIVOPLUVIALES +NOMADISATIONS +NOMENCLATURES +NORMALISAIENT +NORMALISERENT +NOTIFIASSIONS +NOUAKCHOTTOIS +NOURRISSANTES +NUCLEARISEREZ +NUCLEOTIDIQUE +NUMEROTASSIEZ +NUMEROTERIONS +NYCTAGINACEES +OBJECTIVERAIT +OBOMBRASSIONS +OBOMBRERAIENT +OBSCURANTISME +OBSCURCIRIONS +OBSCURCISSENT +OBSESSIONNELS +OBTEMPERAIENT +OBTEMPERERAIT +OCCULTASSIONS +OCCUPATIONNEL +ODONTALGIQUES +OEILLETONNERA +OLEANDOMYCINE +OLIGOELEMENTS +OLIGOPHRENIES +OMBRAGERAIENT +OPINIATRERONS +OPPOSABILITES +OPTIMALISEREZ +OPTIMISASSENT +ORCHESTRERAIT +ORDINATICIENS +ORDONNANCERAI +ORGANIQUEMENT +ORGANISATEURS +ORGANISERIONS +ORGANSINAIENT +ORIENTALISENT +ORIENTALISTES +ORTHOPEDISTES +OSCILLATOIRES +OSCILLOGRAMME +OSTENTATOIRES +OSTEOMALACIES +OSTRACISERAIT +OUTRECUIDANTS +OUVRAGERAIENT +OVATIONNERONT +PACIFICATEURS +PACTISASSIONS +PAGNOTASSIONS +PAGNOTERAIENT +PAILLARDERAIT +PAILLETTERAIT +PAILLONNAIENT +PALEOCHRETIEN +PALETHNOLOGIE +PALISSADASSES +PALISSADERONS +PALISSONNASSE +PALMATISEQUEE +PANEUROPEENNE +PANGERMANISTE +PANOPHTALMIES +PANORAMIQUAIS +PANORAMIQUENT +PANTHEONISANT +PARACHEVERONT +PARACHRONISME +PARACHUTERONS +PARAFFINERENT +PARAGUAYENNES +PARALYSASSIEZ +PARAPHLEBITES +PARAPHRASASSE +PARASEXUALITE +PARATONNERRES +PARCELLARISEZ +PARCELLISAMES +PARCELLISERAS +PARCELLISIONS +PARCHEMINERIE +PARDONNASSIEZ +PAREMENTERENT +PARFONDISSIEZ +PARISIANISENT +PARISIANISTES +PARKERISATION +PARKERISERAIS +PARKINSONIENS +PARODONTOLYSE +PARQUETTERIEZ +PARSEMASSIONS +PARSEMERAIENT +PARTAGEASSIEZ +PARTICIPANTES +PARTICIPERONT +PARTISANERIES +PARTOUSERIONS +PASSERILLAGES +PASSIONNISTES +PASTELLASSENT +PASTELLERIONS +PASTEURISERAI +PASTILLASSENT +PASTILLERIONS +PATOUILLERONT +PATRIGOTERAIS +PATRONNASSENT +PATRONNERIONS +PATROUILLERAI +PAUMOIERAIENT +PAUPERISAIENT +PAUPERISERONS +PECLOTERAIENT +PEINTURASSENT +PEINTURERIONS +PEINTURLURONS +PELLETISATION +PELOTONNERAIS +PENALISASSIEZ +PENALISERIONS +PENITENTIELLE +PENSIONNAIRES +PENSIONNERONT +PENTADACTYLES +PEOPOLISATION +PERCEPTUELLES +PERENNISASSES +PERENNISERIEZ +PERFECTIONNAS +PERICLITERONT +PERIPHRASAMES +PERISCOLAIRES +PERISPLENITES +PERISTALTIQUE +PERMACULTURES +PERMANENCIERE +PERMEABILITES +PEROXYDASSENT +PERPETRATIONS +PERPETUATIONS +PERPETUERIONS +PERSECUTERAIS +PERSEVERERONT +PERSIFFLERENT +PERSIFFLEUSES +PERSILLASSENT +PERTURBATIONS +PERVIBRASSENT +PETARDASSIONS +PETARDERAIENT +PETRARQUISIEZ +PHALLOCRATIES +PHARMACIENNES +PHARMACOLOGUE +PHARYNGOSCOPE +PHASCOLOMIDES +PHILANTHROPES +PHILATELISTES +PHILISTINISME +PHILOSOPHAMES +PHLOGISTIQUES +PHOSPHATURIES +PHOSPHORERENT +PHOTOCATALYSE +PHOTOCOMPOSER +PHOTOCOPIAMES +PHOTOCOPIEREZ +PHOTOELECTRON +PHOTOLECTURES +PHOTOSYNTHESE +PHOTOTHERAPIE +PHYLLOXERIQUE +PHYTOTOXICITE +PIAPIATASSIEZ +PICTORIALISME +PIETONNISASSE +PIEZOMETRIQUE +PIGMENTASSENT +PIGOUILLASSES +PIGOUILLERONS +PINCEAUTERAIS +PIQUENIQUASSE +PIQUENIQUERAI +PIXELLISATION +PIXELLISERAIS +PLACARDASSIEZ +PLACARDISATES +PLACARDISERAS +PLACARDISIONS +PLANCHEIERAIS +PLAQUEMINIERS +PLASTICULTURE +PLASTIFIERONT +PLASTIQUASSES +PLASTRONNATES +PLASTRONNERAS +PLASTRONNIONS +PLATYRHINIENS +PLEUVASSERAIT +PLOUTOCRATIES +PLURALISERENT +PNEUMECTOMIES +PODCASTASSENT +PODCASTERIONS +POETISASSIONS +POIGNARDERAIS +POINCONNEMENT +POINCONNERENT +POINCONNEUSES +POINTILLERAIS +POIREAUTERENT +POLEMIQUERENT +POLEMONIACEES +POLITIQUASSES +POLITIQUERONT +POLYACRYLIQUE +POLYCONDENSAT +POLYCOPIERIEZ +POLYGONATIONS +POLYTECHNIQUE +POLYVINYLIQUE +POMERANIENNES +PONDERABILITE +PORTAGERAIENT +PORTEFEUILLES +PORTEMONNAIES +PORTIONNAIRES +PORTLANDIENNE +PORTRAITURAIS +POSITIONNEREZ +POSITIONNIONS +POSTERISASSES +POSTERISERONS +POSTILLONNONS +POSTSONORISAI +POSTSONORISEE +POSTULASSIONS +POTENTIALISAS +POUPONNASSENT +POUPONNERIONS +POURCHASSERAI +POURLECHERENT +POURRISSABLES +POURSUIVISSES +PREAFFRANCHIE +PREAVISASSENT +PREAVISERIONS +PRECAMBRIENNE +PRECARISAIENT +PRECARISERONS +PRECAUTIONNAS +PRECISERAIENT +PRECOMMANDONS +PRECONISAIENT +PRECONISERENT +PRECONTRAINTS +PREDECESSEURE +PREDELINQUANT +PREDISPOSAMES +PREDOMINERAIS +PREFABRIQUAIS +PREFIGURAIENT +PREFIGURERONS +PREFINANCATES +PREFIXASSIONS +PREFLORAISONS +PREINSCRIRAIT +PREINSCRIVONS +PREJUDICIATES +PREJUGERAIENT +PREOCCUPANTES +PREOCCUPATION +PREOCCUPERAIS +PREOPERATOIRE +PREROMANTIQUE +PRESBYACOUSIE +PRESCRIPTIVES +PRESCRIVAIENT +PRESENTIELLES +PRESERVATIONS +PRESONORISAIT +PRESSENTIRIEZ +PRESSURISASSE +PRETERITERIEZ +PRETINTAILLES +PREVISIBILITE +PRIORISASSIEZ +PRIORISERIONS +PRIVATISATION +PRIVATISERAIS +PRIVILEGIASSE +PROBLEMATISER +PROCEDERAIENT +PROCREERAIENT +PRODUISISSIEZ +PROFANASSIONS +PROFESSORALES +PROFITERAIENT +PROGRAMMERAIT +PROGRESSASSES +PROGRESSISTES +PROJETASSIONS +PROLETARISAIT +PROLIFERERAIT +PROLONGEAIENT +PROMULGUASSES +PROMULGUERONS +PRONOSTIQUAIS +PRONOSTIQUIEZ +PROPEDEUTIQUE +PROPHARMACIEN +PROPITIATIONS +PROPRIOCEPTIF +PROSCRIPTIONS +PROSENCEPHALE +PROSPERASSENT +PROSTERNASSES +PROSTERNEMENT +PROSTERNERENT +PROTEGERAIENT +PROTHROMBINES +PROTOHISTOIRE +PROTOPLANETES +PROTOTYPAIENT +PROVIDENTIELS +PROVIGNASSIEZ +PROVIGNERIONS +PROVISIONNONS +PROVOCATRICES +PSALMODIERIEZ +PSYCHASTHENIE +PSYCHIQUEMENT +PSYCHOMOTRICE +PTERIDOPHYTES +PULSIONNELLES +PULVERISAIENT +PULVERISERIEZ +PURIFICATOIRE +PUTREFIASSENT +PUTREFIERIONS +PYROCLASTIQUE +PYROGALLIQUES +PYROGRAVERONT +PYROLYSASSIEZ +QUADRILLERIEZ +QUANTIFIABLES +QUANTIFIASSES +QUANTIFIERENT +QUEBECISAIENT +QUESTIONNERAI +QUINCAILLIERE +QUINTOYASSENT +QUINTUPLERENT +RABOUDINERAIS +RACCOURCIRONS +RACCOUTUMAMES +RACCOUTUMEREZ +RADICALISERAI +RADIOBALISAGE +RADIOBALISIEZ +RADIOGRAPHIES +RADIOTHERAPIE +RADOUCISSIONS +RAFISTOLERONT +RAIDISSEMENTS +RALENTISSIONS +RALLONGEAIENT +RANDOMISATION +RANDOMISERAIS +RAPATRIASSENT +RAPATRIEMENTS +RAPATRONNASSE +RAPETASSERAIT +RAPLOMBASSENT +RAPLOMBERIONS +RAPPAREILLAIS +RAPPARIASSIEZ +RAPPARIERIONS +RAPPOINTIRAIS +RAPPORTASSIEZ +RASSORTISSANT +RATATINASSIEZ +RATATOUILLAIT +RATIBOISERAIS +RATIOCINAIENT +RAUGMENTERENT +REABONNASSIEZ +REABONNERIONS +REABSORBAIENT +REACTIVASSENT +REACTUALISAIT +READAPTASSIEZ +READAPTERIONS +READMETTAIENT +READMETTRIONS +REAFFIRMERENT +REAGENCASSENT +REAGENCEMENTS +REALIMENTATES +REALIMENTERAS +REALIMENTIONS +REAPPROPRIAIT +REARRANGERONT +REASSIGNASSES +REASSIGNERIEZ +REATTRIBUATES +REATTRIBUERAS +REATTRIBUIONS +REBLANCHIRENT +REBOBINASSENT +REBOBINERIONS +REBRANCHERENT +REBRULASSIONS +REBRULERAIENT +RECACHETASSES +RECALCIFIEREZ +RECALIBRERENT +RECAPITALISAS +RECAUSASSIONS +RECHAPPASSIEZ +RECHARGERIONS +RECHAUFFAIENT +RECHAUFFERONS +RECHIGNASSENT +RECHIGNEMENTS +RECIPROQUEREZ +RECOIFFASSENT +RECOIFFERIONS +RECOMBINASSES +RECOMBINERONS +RECOMMENCATES +RECOMPARAITRE +RECOMPOSABLES +RECOMPOSASSES +RECOMPOSERONS +RECOMPTASSENT +RECOMPTERIONS +RECONCILIASSE +RECONCILIERAS +RECONCILIIONS +RECONDAMNASSE +RECONDUISIMES +RECONFIGUREES +RECONFORTATES +RECONFORTERAS +RECONFORTIONS +RECONNAITRONS +RECONNECTASSE +RECONSTITUANT +RECONSTRUIRAS +RECONSTRUISIT +RECONTACTATES +RECONTACTERAS +RECONTACTIONS +RECONVERTIMES +RECONVOQUAMES +RECONVOQUEREZ +RECORRIGEASSE +RECRIMINATEUR +RECTIFIASSENT +RECTIFICATION +RECTIFIERIONS +RECUEILLERAIT +RECUEILLISSES +RECULOTTERIEZ +RECUPERATEURS +REDECOUPERAIS +REDEFINISSANT +REDEMANDERIEZ +REDEPLOIERAIS +REDESCENDITES +REDESCENDRIEZ +REDESSINERONT +REDIFFUSAIENT +REDIGEASSIONS +REDISCUTERONT +REDISTRIBUIEZ +REDUPLICATIVE +REDYNAMISEREZ +REEDIFIASSENT +REELIGIBILITE +REEMPLOIERIEZ +REENGAGEMENTS +REENREGISTREZ +REENSEMENCANT +REENSEMENCENT +REENTENDIRENT +REESCOMPTAMES +REESCOMPTEREZ +REESSAIERIONS +REEXECUTAIENT +REFACONNERONT +REFACTURAIENT +REFERENCERAIT +REFERMENTEREZ +REFINANCAIENT +REFINANCERONS +REFONDATRICES +REFORMULERAIS +REFOUILLERAIT +REFOURGUERAIT +REFRIGERATION +REFRIGERERAIS +REFROIDISSAIT +REGAZONNERENT +REGENERATIVES +REGIONALISIEZ +REGREFFASSENT +REGREFFERIONS +REGRESSASSENT +REGULARISAMES +REGULARISERAS +REGULARISIONS +REHABILITERAI +REIMPLANTASSE +REIMPRIMERONT +REINCARCERAIT +REINITIALISAI +REINITIALISEE +REINSCRIVITES +REINTEGRERONT +REINTERPRETER +REINTRODUISEZ +REINVESTIRAIT +REINVITASSENT +REINVITERIONS +REJOINTOIERAI +REJOUISSANTES +RELARGIRAIENT +RELARGISSIONS +RELATIONNELLE +RELEGUASSIONS +RELEGUERAIENT +RELOCALISASSE +RELUISISSIONS +REMACHASSIONS +REMACHERAIENT +REMARCHASSIEZ +REMBARQUEMENT +REMBARQUERENT +REMBARRASSENT +REMBARRERIONS +REMBAUCHAIENT +REMBOURRASSES +REMBOURRERONS +REMBOURSERAIS +REMEMORATIONS +REMILITARISER +REMMAILLERAIS +REMMANCHASSES +REMMANCHERONS +REMOBILISATES +REMORQUASSIEZ +REMPAQUETTERA +REMPOISSONNES +RENCAISSERAIS +RENCHAUSSASSE +RENCHERISSONS +RENCONTRASSES +RENCONTRERONS +RENDORMISSENT +RENDOSSASSIEZ +RENEGOCIERENT +RENFLAMMERAIS +RENFLOUASSIEZ +RENFLOUERIONS +RENFORCASSIEZ +RENFORCEMENTS +RENFORCISSANT +RENFORMISSAIS +RENFROGNERAIT +RENGORGEASSES +RENSEIGNEMENT +RENSEIGNERENT +RENTABILISANT +RENTABILISENT +RENTRAIRAIENT +RENUMEROTAMES +RENUMEROTEREZ +REORDONNERENT +REOUVRIRAIENT +REOXYGENERONT +REPAIRERAIENT +REPARLASSIONS +REPARLERAIENT +REPEIGNISSENT +REPENTISSIONS +REPERCUTERAIT +REPOSITIONNEZ +REPRECISAIENT +REPRESENTABLE +REPRESENTANTS +REPRESENTERAI +REPROGRAMMERA +REQUINQUERAIS +REQUISITOIRES +RESITUASSIONS +RESITUERAIENT +RESOCIALISEES +RESPIRATOIRES +RESPLENDIRAIT +RESQUILLAIENT +RESSOUDASSENT +RESSOUDERIONS +RESSOURCAIENT +RESSOURCERIEZ +RESSURGISSONS +RESTRUCTURIEZ +RESULTERAIENT +RESURCHAUFFAT +RETAPISSERIEZ +RETEIGNISSENT +RETELEPHONANT +RETORQUASSIEZ +RETRANCHASSES +RETRANCHERIEZ +RETRANSMETTRE +RETRAVERSERAI +RETREMPASSENT +RETREMPERIONS +RETROCEDERONT +RETROSPECTIFS +RETROUSSERONT +RETROUVASSENT +RETROUVERIONS +REUNIFICATION +REUNIONNAISES +REVALORISAMES +REVALORISERAS +REVALORISIONS +REVASSASSIONS +REVEILLONNANT +REVEILLONNEUR +REVERBERERONT +REVERENCIEUSE +REVERIFIAIENT +REVISITASSIEZ +REVIVISCENCES +REVOLVERISERA +REVOQUASSIONS +REVOQUERAIENT +RHETORIQUEURS +RHINOPLASTIES +RIBOULASSIONS +RIBOULERAIENT +RICKETTSIOSES +RIDICULISERAI +RIGIDIFIAIENT +RIPAILLASSENT +RISTOURNERAIT +RITUALISAIENT +RITUALISERONS +ROBINETTERIES +ROBOTICIENNES +ROBOTISATIONS +ROGNONNASSIEZ +ROMANISASSENT +RONCHONNASSES +RONCHONNERIEZ +RONDOUILLARDE +RONRONNASSIEZ +RONSARDISATES +RONSARDISEREZ +ROUSPETASSENT +ROUSSELASSENT +ROUSTISSAIENT +RUISSELASSIEZ +RUISSELLERAIT +RUSSIFIASSENT +SABELLIANISME +SACCHARIFIENT +SALIFICATIONS +SALPETRASSENT +SANCTIFIASSES +SANCTIONNAMES +SANCTIONNEREZ +SANDWICHERIES +SANGLOTASSENT +SANGLOTERIONS +SAPONIFIERAIT +SATELLISABLES +SATELLISASSES +SATELLISERIEZ +SATELLITAIRES +SATIRISASSENT +SATIRISERIONS +SAUCISSONNERA +SAUCISSONNONS +SAUPOUDRERAIS +SAUTILLEMENTS +SAVANISATIONS +SCANDALISERAI +SCATOLOGIQUES +SCENARISERENT +SCHIZONEVROSE +SCINTILLAIENT +SCISSIONNISTE +SCLEROPHYLLES +SCOLARISERAIT +SCOTOMISERONT +SCRIBOUILLAIT +SCROGNEUGNEUX +SECONDASSIONS +SECULIEREMENT +SEDENTARISANT +SEDENTARISENT +SEDIMENTERENT +SELECTIONNENT +SEMPERVIRENTS +SENEGALISASSE +SEPTICEMIQUES +SERANCASSIONS +SERANCERAIENT +SERICICULTURE +SERIGRAPHIAIS +SERONEGATIVES +SERRAVALLIENS +SERVIABILITES +SEXUALISATION +SEXUALISERAIS +SIGNIFICATION +SILHOUETTAMES +SILHOUETTEREZ +SILICONASSIEZ +SIMPLIFIERAIT +SINAPISASSIEZ +SINGULARISANT +SINGULARISENT +SOCIOBIOLOGIE +SOCIOLOGISTES +SODOMISASSIEZ +SOLENNISAIENT +SOLIDIFIERENT +SOLIFLUCTIONS +SOLUBILISEREZ +SOLUTIONNATES +SOLUTIONNERAS +SOLUTIONNIONS +SOLVABILISAIS +SOMATISASSIEZ +SOMATISERIONS +SOMMEILLAIENT +SOMMEILLERONT +SOMNOLERAIENT +SONNAILLERONS +SOUDOIERAIENT +SOUFFLETTERAI +SOUMETTRAIENT +SOUMISSIONNES +SOUPCONNERAIT +SOUPCONNEUSES +SOUPIRERAIENT +SPECTACULAIRE +SPECTROMETRES +SPELEOLOGIQUE +SPHEROIDIQUES +SPHYGMOGRAPHE +SPIRITUALISME +SPONSORISASSE +SPRECHGESANGS +SPRINTASSIONS +SQUATTERISENT +STABILIMETRES +STABILISERONS +STALAGMOMETRE +STENODACTYLOS +STENOTYPERONT +STERILISANTES +STERILISATEUR +STOMATORRAGIE +STOMISASSIONS +STOMISERAIENT +STRANGULERAIT +STRATOVISIONS +STREPTOCOCCIE +STRIDULASSENT +STRIPTEASEURS +STROMATOLITHE +STRUCTURERAIT +STRUTHIONIDES +SUBCONSCIENCE +SUBJECTIVISTE +SUBLIMINAIRES +SUBMERGEAIENT +SUBORDONNANTE +SUBROGERAIENT +SUBSTANTIVERA +SUBSTITUAIENT +SUBSTITUERONT +SUBVERTISSAIS +SUBVIENDRIONS +SUCCEDASSIONS +SUCCINCTEMENT +SULFINISATION +SULFONERAIENT +SULFOVINIQUES +SUPERBENEFICE +SUPERPOSABLES +SUPERPOSASSES +SUPERPOSERONS +SUPERVISERAIT +SUPPLANTATION +SUPPLANTERAIS +SUPPLEMENTONS +SUPPURERAIENT +SURABONDERAIS +SURACTIVERAIS +SURAJOUTERIEZ +SURALIMENTERA +SURBAISSERAIS +SURCHARGERONS +SURCOMPENSEES +SURCOUPASSENT +SURCOUPERIONS +SURCREUSEMENT +SURDETERMINAT +SURENTRAINAIS +SUREQUIPEMENT +SUREQUIPERENT +SURESTIMAIENT +SURESTIMERONS +SUREXCITERONS +SUREXPLOITAIS +SUREXPOSASSES +SUREXPOSERONS +SURFACTURERAI +SURGEONNERAIS +SURGISSEMENTS +SURGONFLERONT +SURIMPOSERAIS +SURINAMIENNES +SURINVESTIREZ +SURJETASSIONS +SURLENDEMAINS +SURMEDIATISAT +SURMENASSIONS +SURMENERAIENT +SURMORTALITES +SURNAGEASSIEZ +SURNUMERAIRES +SURPRODUCTEUR +SURPRODUIRAIT +SURPRODUISAIS +SURREAGISSAIT +SURSATURERENT +SURSEOIRAIENT +SURSIMULATION +SURVALORISANT +SURVALORISENT +SURVEILLANTES +SURVOLTASSENT +SURVOLTERIONS +SUSPECTASSENT +SUSPECTERIONS +SYLVICULTEURS +SYMBOLISAIENT +SYMBOLISERONS +SYMPATHISAMES +SYNCHRONICITE +SYNCHRONISAIT +SYNCHRONISIEZ +SYNTHETISASSE +SYNTONISATEUR +SYNTONISERONT +SYPHILIGRAPHE +SYSTEMATIQUES +TACHYPSYCHIES +TAILLADASSIEZ +TALONNERAIENT +TAPAGEASSIONS +TARIFIERAIENT +TATAOUINERIEZ +TAYLORISAIENT +TAYLORISERONS +TCHAPALOTIERE +TECHNISASSIEZ +TECHNOCRATIES +TECHNOCRATISE +TELEACHETEUSE +TELECHARGEURS +TELECOMMANDES +TELEDECLAREES +TELEDIFFUSANT +TELEGUIDERIEZ +TELEPHONERIEZ +TELEPORTAIENT +TELEPORTERONS +TELESCOPERAIS +TELEUTOSPORES +TELEVISASSENT +TELEVISERIONS +TEMPORISERONS +TENORISASSIEZ +TERATOGENESES +TERMINOLOGUES +TERRORISERIEZ +TESTICULAIRES +TETANISASSIEZ +TETANISERIONS +TETRODOTOXINE +THANATOLOGUES +THAUMATURGIES +THEATRALISANT +THEATRALISENT +THEOCENTRISME +THERMISASSENT +THERMOCOLLANT +THERMOFORMAGE +THERMOFORMIEZ +THIOCARBONATE +THORACENTESES +THROMBOPOIESE +THYROTROPINES +TICTAQUASSIEZ +TINTINNABULAI +TITILLERAIENT +TITRISASSIONS +TITULARISASSE +TONITRUERIONS +TORAILLASSIEZ +TORCHONNASSES +TORCHONNERONS +TORRAILLERONS +TORREFACTEURS +TORTILLASSIEZ +TOURMENTANTES +TOURNEBOULAIS +TOUSSAILLASSE +TOUSSAILLERAI +TRACASSASSENT +TRAFICOTERIEZ +TRAINAILLERAI +TRAINASSERAIS +TRAMPOLINISTE +TRANSAFRICAIN +TRANSBAHUTONS +TRANSCANADIEN +TRANSCENDATES +TRANSCENDERAS +TRANSCENDIONS +TRANSCODERENT +TRANSCRIVIONS +TRANSCULTUREL +TRANSDUCTEURS +TRANSFILERAIT +TRANSFUSERIEZ +TRANSGRESSANT +TRANSGRESSIVE +TRANSHUMERONT +TRANSIGEASSES +TRANSISSAIENT +TRANSLATERONT +TRANSLITTERER +TRANSLOCATION +TRANSMETTRONS +TRANSMIGRASSE +TRANSMUTERONS +TRANSPARAITRE +TRANSPERCASSE +TRANSPLANTEES +TRANSPLANTOIR +TRANSPORTABLE +TRANSPORTIONS +TRANSSONIQUES +TRANSSUDERAIT +TRANSURANIENS +TRANSVIDASSES +TRANSVIDERONS +TRAVAILLOTIEZ +TRAVERSASSENT +TRAVERSERIONS +TRAVESTISSAIT +TREILLAGERONT +TREILLISSASSE +TREMBLOTERONT +TREUILLASSIEZ +TRIANGULERONT +TRIBUNITIENNE +TRIFOUILLASSE +TRIMBALLASSES +TRIMBALLERIEZ +TRIOMPHALISME +TRIPATOUILLAS +TRISANNUELLES +TRONCONNASSES +TRONCONNERIEZ +TROPHOBLASTES +TROPICALISAIS +TROPICALISMES +TROUILLOMETRE +TRUFFICULTURE +TRUSQUINERIEZ +TUBERCULINERA +TUBERCULISANT +TUBERCULISENT +TUMEFIASSIONS +TUMEFIERAIENT +TURBINERAIENT +TURLUPINAIENT +TYMPANISASSES +TYMPANISERONS +TYPOGRAPHIAIS +TYRANNISAIENT +UNIFORMISEREZ +UNIVERSALISME +UNIVIBRATEURS +URBANISATIONS +USUFRUITIERES +VAGABONDERENT +VALDINGUERONT +VALDOISIENNES +VALERIANELLES +VALETUDINAIRE +VANDALISERONT +VANITEUSEMENT +VAPORISERIONS +VASOPRESSINES +VASOUILLERAIS +VATICINATEURS +VAUDEVILLISTE +VEDETTISAIENT +VEDETTISERONS +VERBIGERATION +VERDOIERAIENT +VERDUNISATION +VERDUNISERAIS +VERMILLONNONS +VERROUILLATES +VERROUILLERAS +VERROUILLEUSE +VERTUEUSEMENT +VIABILISASSES +VIABILISERIEZ +VIBROMASSEURS +VIDEOGRAPHIES +VIGNETTERIONS +VILLAGISATION +VILLEGIATURER +VIOLONERAIENT +VIRAILLERIONS +VIREVOLTERAIT +VIRGINALEMENT +VIROSTATIQUES +VIRTUELLEMENT +VISIOPHONIQUE +VISUALISATION +VISUALISERAIS +VITAMINASSENT +VITAMINERIONS +VITRIFIASSENT +VITUPERATEURS +VIVIFICATEURS +VOCIFERATEURS +VOISINERAIENT +VOLATILISEREZ +VOLCANISERAIT +VOLCANOLOGUES +VOLONTARISTES +VULCANISERENT +VULGARISASSES +VULGARISERAIT +VULNERABILISA +YOUYOUTASSENT +ZIGOUILLAIENT +ZIGUINCHOROIS +ZIGZAGUASSIEZ +ZINZINULERONT +ZOOTECHNICIEN +abandonniques +abasourdiriez +abreviatrices +abrutissantes +abrutisseuses +absentassions +abyssiniennes +academicienne +acagnardaient +accastillages +accastillerai +accelererions +accessoiriser +accessoiriste +accidenterais +accomplissiez +accouchassiez +accoucherions +accourcissait +accoutumerais +accrediteront +accrochassent +accrochements +accueilleront +accueillirent +acculturerent +accumulassent +achromatisera +acousticienne +acquiescaient +actinomycoses +actionnassiez +actionnerions +actualiserons +acupunctrices +additionneurs +adjectivaient +admonesteront +adverbialisat +aerostatiques +affaiblissant +affaireraient +affaissassiez +affaisserions +affectionnons +affermeraient +affleurassiez +affleurerions +affligeraient +affouagerions +affouillasses +affouilleriez +affourcherais +affourragerez +affranchirent +affrianderont +africanisates +africanthrope +agenouillerai +agglomereront +agglutinerent +aggravassions +agrementeriez +agrobiologies +agrochimiques +agropastorale +agrotourismes +aheurteraient +aiguilletions +aiguillonnant +aiguillonnent +alcaliniserai +alcoolomanies +algorithmique +alimentassiez +allaiteraient +allergologies +alliterations +allongeassiez +alluvionnames +alluvionneras +alluvionnions +alourdiraient +alphabetisait +alphabetisiez +amaryllidacee +ambiancerions +ambitionnames +ambitionnerez +ameliorateurs +amenuisassiez +amenuiserions +americanisiez +amincissaient +amoindrissant +amoncelassent +amortissables +amourachaient +anacardiacees +anachroniques +anastomoserai +anencephalies +anesthesiques +angiocholites +angoissassiez +angustifoliee +animadversion +animaliserais +annihilations +annonciations +anovulatoires +anthropiserai +anthropophile +antiacneiques +antibiotiques +anticiperions +antidemarrage +antidetonante +antigivrantes +antiliberales +antiparallele +antireflexifs +antisismiques +antithermique +aplacentaires +apophantiques +apoplectiques +appareillerez +appauvrissiez +appendissions +appertisaient +appertiserons +appesantiriez +appesantisses +applaudissant +applaudissiez +appointassiez +appointirions +appreciatrice +apprivoiserai +approfondirai +approprierent +approvisionna +approximation +apragmatismes +aquarellerent +aquitaniennes +arabisassions +arcboutassent +arcbouterions +archimedienne +architectonie +argumentateur +arithmographe +aromatherapie +arrondissages +artificialise +artificialite +arytenoidiens +ascensionnais +ascensionniez +asomatognosie +aspergeassiez +asphaltassent +asphalterions +asphyxiassent +asphyxierions +assaillissent +assaisonnasse +assassinasses +assecheraient +assermenterez +asservissante +asservisseuse +assignassions +assimilateurs +assombrissiez +assommassions +assommeraient +assoupiraient +assouplissais +assouplisseur +assourdissons +assouviraient +astreignisses +astrometriste +astrophysique +asynchronisme +atheromateuse +attardassions +attarderaient +atteindraient +aurifications +authentiquera +autoadhesives +autoanalysais +autocassables +autocensurait +autocensurees +autodetruisez +autodissolves +autodissoudre +autoevaluames +autoevalueras +autoevaluions +autofinancons +autoflagelles +autographiiez +autoproduirai +autopromotion +autoprotolyse +autosatisfait +avalisassions +avaliseraient +avantageasses +avertissement +avilissements +avironnassent +avocassassiez +bacteriolyses +badigeonnions +bafouillerait +bagarrassions +bagarreraient +baillonneront +baisotassions +baisoteraient +bambouseraies +banalisations +bancarisation +bancariserais +banderilleros +banquetassiez +baraquassions +barbarisasses +barbariserons +barbouillions +barguignerons +barometriques +barricaderais +basculassions +bataillassiez +bathymetrique +beauvaisienne +becheveteriez +becquetassiez +behavioristes +bellettrienne +bellifontains +bellinzonaise +bemolisassent +bemoliserions +beneficiaient +beneficiasses +berriasiennes +bibelotassiez +biberonneriez +bicameralisme +biculturelles +bienveillants +bigarreautier +bigophonerent +bilateralisme +biloquassions +biloqueraient +biocarburants +bioconversion +biogenetiques +bipassassions +bipasseraient +biquotidienne +bistourneront +bivouaquerais +blackboulages +blackboulerai +blacklistames +blacklisterez +blanchissante +blasonnassent +blasonnerions +blatereraient +bonimenterent +bonimenteuses +bordeliserent +bordurassions +bordureraient +bouboulassent +bouchardasses +boucharderons +bouchonnerais +bouffonnaient +bouffonnerais +bouffonneront +bouilliraient +bouillonnante +bouleversasse +bourdonnaient +bourdonnieres +bourgeonnates +bourgeonneras +bourgeonnions +bourrelassiez +boursoufflage +boursouflerai +bousillassent +bousillerions +boycottassent +boycotterions +brachiosaures +brandeviniers +bredouillante +bredouilleurs +bresillassiez +bretaillerent +brettelleront +brillanterait +brillantinera +bringuassions +bringuebalant +brinquebalent +bronchassions +broncheraient +bronchospasme +brouillassait +brouillerions +bruissassions +bruisseraient +brunchassions +brusqueraient +brutaliserent +budgetassions +budgeteraient +budgetiserait +buissonnaient +buissonneront +bunkerisasses +bunkeriserons +bureaucratisa +cabotinassiez +cafardassions +cafarderaient +cafouillerons +caillasseront +cailloutasses +caillouterons +calculabilite +calorifugeais +calorifugerez +calorisations +calottassions +calotteraient +camboulassent +camboulerions +cambrioleront +camphrassions +camphreraient +campigniennes +canadianisent +cancellassiez +cancerisation +canceriserais +canonisassiez +canoniserions +cantonalisant +cantonalisent +cantonalistes +caoutchoutent +caparaconnent +capitonneriez +capitulations +caporaliserai +capparidacees +caquetterions +caracterisera +carambolerais +carambouiller +carameliserai +caravagesques +carbonataient +carbonaterons +carbonitrurez +carcaillasses +cardiographes +carpicultures +carrelassions +cartellisasse +cartoucherais +cartoucheront +casematassiez +cataleptiques +catapulterais +catechiserait +catecholamine +categoricites +categorisasse +catholicisent +cauchemardais +cauchemardent +cauchemardons +centraliserez +centrifugeras +centrifugeuse +cephalocordes +cerificateurs +certifiassiez +certifierions +cesalpiniacee +cesarisassent +cesariserions +chamaillasses +chamailleriez +chambrassions +champleveriez +chancelleriez +chanfreinasse +chansonnasses +chansonnerons +chantonnasses +chantonneriez +chantournasse +chapeauteriez +charbonneries +chardonnasses +chardonnerets +charnellement +charpentaient +charpenterons +charrioteront +charroyassiez +charruassions +charrueraient +chataignerait +chatouilleras +chaudronniere +cheminassions +chenopodiacee +chevaleraient +chevauchement +chevaucherent +chevrefeuille +chiffonnaient +chiffonnerons +chiffreraient +chirographies +chiropracties +chloracetique +chlorophylles +chondrichtyen +chondroblaste +choregraphiai +choregraphiee +chouannassiez +chourinassent +chourinerions +chroniquerait +chronometrera +cicatrisables +circoncissent +circonscrirez +circonscrivis +circonstancie +circularisais +circuleraient +cisaillassiez +cisaillerions +clactoniennes +claironnerais +clairsemerait +claquetassiez +classifiasses +classifierait +claudiqueriez +claustrations +clavetterions +claviculaires +clericalisent +clignotements +climatiserait +cliquetterait +clochardisera +cloisonnaient +cloisonnerons +cloturassions +clotureraient +coadjutorerie +coalescerions +coalisassions +coaliseraient +cobaltherapie +codetentrices +codirigeasses +cogenerations +cogniticienne +cognitivismes +cohabitassiez +coincidassent +colibacillose +collaboreriez +collectionner +collectivisez +colletaillais +colligeassent +colloquassent +colloquerions +colmatassions +colmateraient +colonisations +colpoplasties +combientiemes +commenterions +commissionnai +commissionnez +communication +communierions +commuteraient +comparaissant +comparaitront +comparussions +compatibilite +compenetrates +competiraient +competissions +competitrices +complaisantes +complementera +compograveurs +compostassiez +compressaient +compromettant +compromettons +compromissiez +comptabilisai +comptabilisee +compulsations +concentrerons +conceptualise +concertassiez +concerterions +conchylicoles +conciliabules +concreterions +concretisasse +concupiscents +concurrencent +concurrentiel +condensateurs +condescendent +conditionnant +confectionnes +confedereront +conferenciers +confessionnal +configurerent +confirmations +confitureries +confortassent +conforterions +congeleraient +congestionnez +conglomerasse +conglutinants +conglutinerai +congratulerai +congreganisme +conjecturerez +conjoignaient +conjoindrions +connaissables +consciencieux +conscientisas +conscriptions +conseillerais +conseilleront +consequemment +conservatrice +consideration +considererons +consonantique +consonassions +conspirassent +conspirerions +conspuassions +conspueraient +constellation +constellerais +consterneront +constipassiez +constiperions +constituaient +constrictions +constructeurs +consulterions +contactassiez +contactologue +contemplerent +conteneurisez +contentassent +contentements +contestateurs +contextualise +continuerions +contournement +contournerent +contracteriez +contracturais +contraignions +contraindrons +contrariasses +contrarierons +contrarotatif +contrebandier +contrebassons +contrebraquez +contrecarriez +contrefaisons +contrefichiez +contremandera +contreplaquez +contretypates +contretyperas +contretypions +contreventiez +contribueriez +contributeurs +controuverait +controversent +convainquisse +convalescente +conventualite +conversations +convivialites +convolvulacee +convulsassent +convulserions +convulsionnai +convulsionnez +coorganisames +coorganiserez +copartageasse +copenhagoises +copermuterait +copresidasses +cordonnassiez +cordonnerions +correspondant +cosigneraient +cosmographies +costaricaines +couillonnasse +coupaillerent +coupassassiez +coupellassent +couperosaient +courbatturera +courroucerent +couturassions +coutureraient +covoiturerais +crachouilliez +cramponnasses +cramponneriez +craquetassent +crayonnassent +crayonnerions +cressiculture +cretinisaient +criminalisera +cristalleries +cristallinien +cristallisait +cristoliennes +croquignolets +croupionnames +croustilleuse +crucifiassiez +crucifierions +cryochirurgie +cryogenisames +cryogeniseras +cryogenisions +cryoscopiques +cumulostratus +curetteraient +customiserait +cyanosassions +cyanoseraient +cyanureraient +cyclosportive +cyclotourisme +cypriniformes +cystographies +cytosquelette +damasquinerie +dandinassions +dansotassions +dansotterions +debadgeassent +debagouleront +debaillonnera +debalourdames +debalourderez +debaptiserait +debauchassent +debaucherions +debenzolerent +debillardates +debillarderas +debillardions +deblaieraient +debobinassiez +deboguassions +debogueraient +debossellerai +debourrassent +debourrements +deboussolames +deboussolerez +debraguettais +debranchasses +debrancheriez +debreakassiez +debridassions +debrochassiez +debrouillames +debroussaille +debrousseront +debuchassions +debucheraient +debudgetisait +debusquassent +debusquements +debutanisates +debutaniseras +debutanisions +decadenassais +decalaminames +decalaminerez +decalcifiasse +decalotterait +decanilleriez +decapellerait +decapsulerent +decarbonatais +decarboxylase +decarcassames +decarcasserez +decarceration +decentrerions +decerebrerent +decervelasses +decervelleras +dechausseront +dechiffonnant +dechiffrasses +dechiffreriez +dechiquetames +dechoquassiez +decintrassiez +decintrerions +declassifiais +declaveterent +declenchement +declencherent +declencheuses +declinatoires +declineraient +declinquerais +decliquetates +decloisonnais +decoiffassent +decoiffements +decolleraient +decolorassent +decommettront +decommissions +decompenserez +decomplexerai +decomposerais +decompression +deconcentrees +decongelasses +decongeleriez +deconnectates +deconnecteras +deconnections +deconseillais +deconsignasse +deconstipates +deconstiperas +deconstipions +deconstruirez +decontaminees +decontractent +decortiquames +decortiquerez +decortiqueuse +decouplassent +decouplerions +decourageates +decouvririons +decrediterent +decrementasse +decrepitaient +decrepiterons +decrochassiez +decrocherions +deculasseriez +decuscuteuses +dedifferencia +dedommagerais +dedramatisait +defalquassiez +defatiguerais +defaufilerent +defaussassent +defausserions +defavorisames +defavoriserez +defendissions +deferrassions +deficelassiez +defigeassions +defigurations +defiscalisait +deflagrassiez +deflagrerions +deflatassions +deflateraient +defonceraient +deforcassions +deforceraient +deforesteront +defragmentees +defranchiront +defripassions +defriperaient +defroissaient +defruitassiez +degalonnerait +degantassions +deganteraient +degasolinasse +degauchissais +degauchissiez +degazolinames +degazolinerez +degazonnement +degazonnerent +degenerassent +deglinguerent +degobillerait +degoulineront +degourbifiiez +degraisserais +degravoiement +degravoieriez +degrossassent +degrosserions +degrossissant +degrossissons +degroupassent +degroupements +degueulassais +degueulassons +deguillassiez +deguiseraient +degurgitation +degurgiterais +deharnachames +deharnacherez +dehouilleriez +dehoussassent +dehousserions +dejantassions +dejanteraient +dejaugeraient +delabialisant +delabialisent +delabyrinther +delarderaient +delassassions +delegitimerez +deliberatives +delignifiasse +delimitations +delineamentes +delinearisees +delistassions +delisteraient +delivrassions +delivreraient +delphinariums +demagnetisera +demaigrissiez +dematerialise +demazoutaient +demeriterions +demiellassent +demiellerions +demilitarisai +demilitarisee +demineralises +democratisiez +demodulassent +demonstratifs +demontrassiez +demotivassent +demoulassions +demouleraient +demoustiquons +demultipliais +demusellerait +demutisassiez +demutiserions +demyelinisons +demystifiants +deneigeassiez +denicotinisez +denitratation +denitreraient +denitrifiante +denitrifieras +denitrifiions +densification +denturologies +denuclearises +depacsassions +depacseraient +depalisserait +depaquetaient +depaquetterez +departageront +depassionnait +depassionnees +dephosphatons +depigmentates +depoetisaient +depoitraillas +depolitiserez +depolymerisas +depoudrassiez +depouillaient +depouillerons +depreciations +deprendraient +deprogrammais +deproletarisa +deprotegerais +depucellerons +dequalifieras +dequalifiions +derealiserent +dereferencait +dereferencees +derisoirement +dermatoglyphe +dermatomycose +derupitassiez +desaccentuais +desacclimatez +desaccordames +desaccorderez +desacidifions +desacralisons +desadaptasses +desadapteriez +desaffiliates +desagraferent +desagregeates +desagregerait +desajustasses +desajusteriez +desalignaient +desalignerons +desalinisates +desaltereriez +desambiguisas +desamidonnait +desamidonnees +desaminassent +desamorcerent +desangoissera +desappariames +desapparierez +desappointiez +desapprouvant +desarrimasses +desarrimerons +desassemblant +desassimilait +desassortiras +desatellisees +desatomiserai +desechouasses +desechouerons +desectorisons +desempareriez +desempeseront +desemplirions +desenchainiez +desencollames +desencollerez +desencombriez +desencrassera +desenerverait +desenflammait +desenflammees +desenfourniez +desengagerent +desengluasses +desengluerons +desengrenames +desengrenerez +desenivrasses +desenivrerons +desenlacerais +desenlaidirai +desenraieriez +desensablasse +desensimaient +desensorcelez +desentoilages +desentoilerai +desentravasse +desenvaserais +desenveloppas +desenvoutasse +desepaissirez +desequilibrai +desequilibres +desertisation +desesperement +desexcitaient +desexciterons +desexualisais +deshabillasse +deshabituates +deshabitueras +deshabituions +deshonorantes +deshumidifiai +deshumidifiee +deshydratante +designeraient +desillusionna +desincarcerai +desincarceree +desincarnerez +desincorporez +desincrustant +desindexasses +desindexeriez +desinfectames +desinfecteurs +desinhibaient +desinscrirait +desinscrivons +desinsectises +desinstallant +desinstallent +desintegrates +desinteressas +desintoxiques +desobligerent +desocialisera +desodorisasse +desolidarisas +desorbiterait +desosseraient +desoxygenates +dessaoulaient +dessertissait +dessillassiez +dessouchasses +dessoucherons +dessuinterait +destabilisant +destabilisons +destinataires +destituassent +destituerions +destructivite +destructurant +desulfiterais +desulfurerait +desurchauffer +detalonnerent +detapisseriez +deteignissiez +deterioreront +determinaient +determinative +determinerais +detheinassent +detheinerions +detiendraient +detordissions +detoxiqueriez +detractassiez +detraquassent +detraquements +detroussement +detrousserent +detrousseuses +devaluassions +devasteraient +developperait +devernissages +devernissions +deverrouiller +devirginisait +devirginisees +devisagerions +devitrifiames +diagonalisons +dialoguassiez +diaphragmerez +differentient +diffractaient +digitaliserez +digitalisions +dilacerassiez +dilacererions +diligenteront +discompterait +discontinuiez +disconvinsses +discounteront +discriminasse +discutaillais +discutaillent +discutailliez +disgraciasses +disgracierons +disjoncteriez +disparaissais +dispatcherent +dispenserions +disproportion +disqualifions +dissimilitude +dissipatrices +dissolussions +dissonassions +dissuadassiez +distendraient +distillassiez +distinguaient +distordissiez +distrairaient +distribueront +divergeassiez +diversifiasse +divinisations +documentaires +documenterent +dogmatiseront +dollarisation +dollariserais +domestiquates +domestiqueras +domestiquions +domicilierons +douillassions +douilleraient +drageifieront +dramatiserent +drastiquement +droitisassiez +droitiserions +dryopitheques +dualiseraient +dudgeonnaient +duodecimaines +dynamitassent +dysgenesiques +dyskinetiques +dystrophiques +ebouillantage +ebouillantiez +ebourgeonniez +ebouriffasses +ebourifferons +ebranlassions +ebrechassions +ebullioscopie +ecarquillames +ecarquillerez +ecartelassiez +ecartelerions +ecclesiologie +echalassaient +echalasserons +echantillonna +echardonnates +echardonneras +echardonnions +echarogneront +echelonnaient +echelonnerons +echenillasses +echenillerons +echevelassent +echographions +eclaircissais +ecoeurassions +economiseront +ecorniflerent +ecornifleuses +ecourtichates +ecourticheras +ecourtichions +ecouvillonner +ecrivailleras +ecrivailleuse +ecrivasseriez +ectoparasites +ecussonnaient +editionneront +effarouchates +effectuassiez +effondrassiez +effondrerions +egratigneriez +ejaculassions +ejaculatrices +electrifieras +electrifiions +electrisasses +electriseriez +electrocutent +elisabethains +ellipsographe +emaillassions +emailleraient +embarbouillas +embardouflant +embarrassames +embastillerez +emblavassions +embobinassiez +embouquassent +embouquements +embourrassent +embourrerions +embouteillage +emboutissoirs +embraieraient +embranchasses +embrancheriez +embraquassiez +embraseraient +embreveraient +embrigaderais +embringuerent +embroncherait +embrouillames +embrouillates +embryogenique +embugnassions +embugneraient +emmagasinerez +emmaillotates +emmenageaient +emmitonneriez +empaqueteuses +empattassions +empechassions +empetreraient +empochassions +empocheraient +empoisonneras +empoisonneuse +empoissonnera +empoussieriez +empressassent +empressements +empresureriez +empruntassiez +empuantissais +empuantissons +emulsifiantes +encagoulerais +encaissassent +encaissements +encanaillates +encapsulaient +encapsulerons +encastelasses +encastelerons +encastrassent +encastrements +encellulement +encellulerent +enchantassent +enchantements +enchanteresse +enchaperonnas +enchateleront +enchemiseriez +encherisseuse +enchifrenerez +encliquetages +encouragerent +endetteraient +endoloririons +endolorissent +endommageates +enfargeraient +enfirouapates +enfirouaperas +enfirouapions +enfourcherait +enfreignaient +enfreindrions +enfutaillerai +engoncassions +engouffrement +engouffrerent +engourdirions +engourdissent +engrangerions +enharnachames +enharnacherez +enjambassions +enjavelassent +enjavellerais +enjuponnaient +enliassassent +enliasserions +enluminassiez +enquiquinante +enquiquineurs +enregimentons +enregistrerez +enrouillaient +enroulassions +enrubannerent +ensauvageames +ensauvagerait +ensemencement +ensemencerent +ensevelissait +ensoleillerai +entablassions +entenebraient +enterorenales +enthousiasmat +entomologiste +entortillages +entourloupait +entourloupees +entraccusames +entraidassiez +entrainassent +entrainements +entredevorant +entremangions +entremetteurs +entremettrais +entreposerait +entreprendrai +entreprissent +entreregardee +entretaillant +entretiendrez +entretoisasse +entretuerions +entrouvrirons +enucleeraient +environneront +envisageables +envisageasses +epaississeurs +epincelleriez +epiphenomenes +epistemologie +epointeraient +epontillasses +epontillerons +epoularderait +epoussetterai +epoustouflera +epouvanterais +equilibrerait +equivoqueront +erailleraient +ergotherapies +erotologiques +erythropoiese +escagasseriez +escaladassiez +escargotieres +escarrifieras +escarrifiions +esclavageront +escoffiassiez +escomptassent +escompterions +esgourdassent +esgourderions +espoutiraient +espoutissions +esquintassent +esquinterions +estampillages +estampillerai +esterifieront +esthetisasses +esthetiseriez +estomaquerent +estoquassions +estoqueraient +estourbissent +estrapadasses +estrapaderons +estrapassates +estrapasseras +estrapassions +etalinguerait +etalonnassiez +etalonnerions +etancheraient +ethnicisation +ethniciserais +etronconnates +etronconneras +etronconnions +euphorbiacees +euphorisasses +euphoriseriez +eurostrategie +evangeliserez +eveillassions +eveilleraient +eventreraient +exanthemateux +exclamassions +excrementiels +excursionnons +exfiltrations +exfolieraient +exorcisassiez +exorciserions +exosquelettes +expatriassent +expectorerent +experimentiez +expertiserais +expliciterais +exploitassiez +exploiterions +explosassions +exploseraient +exportatrices +expurgeassiez +extemporanees +exterminaient +exterminerent +exterocepteur +extournassiez +extrapolation +extrapolerais +extravagances +extravaguames +extravasaient +extravaserons +fabriquassiez +faineanterent +faisandassiez +falsification +familiarisees +fangotherapie +farandoleriez +farfouillions +fascineraient +fascisassions +faseillassent +felicitassent +feminisassiez +feminiserions +fermentatives +ferrallitique +ferrosilicium +ferrugineuses +fetichisation +fetichiserais +feuilletterez +fiabiliserais +filialiserent +filigraneront +filochassions +filocheraient +financiarisez +fissionnaient +flamboyassent +flancheraient +flatteusement +flemmarderont +fleuretassiez +flinguassions +flingueraient +fluidifierent +fluxionnaires +foisonnassent +foisonnerions +fonctionneras +fonctionnions +forlongeaient +formalisation +formaliserais +foudroyassent +fouetteraient +fourchassions +fourcheraient +fourgonneront +fourmilleriez +fourvoyassent +foutimasseras +foutimassions +fractionnerai +fragiliseront +framboisaient +framboiseraie +framboiserons +franchisaient +franconiennes +frequentables +frequentasses +frequenterait +fretillassent +fretillerions +fricassassiez +frictionnerez +fristouilliez +froebeliennes +froisseraient +froufroutasse +fumeronnasses +fusionnerions +futurologique +gabionnassent +gabionnerions +gadgetiseriez +galactosemies +galvaniseront +gangrenassent +gangrenerions +gargouillames +gargouillerez +garrottassent +garrotterions +gastrectomies +gastroplastie +gazouillaient +gemmologistes +generationnel +geocentrismes +geographiques +geomarketings +geothermiques +germaniseront +germanophobie +gerontocratie +gerontologues +gibberellines +glandouillant +glaviotassent +glavioterions +globicephales +glorifiassent +glycerineront +gobichonnerai +godilleraient +goinfrassions +goinfreraient +gonochorismes +goudronneries +gourmanderais +gournableront +grafignassent +grafignerions +graillassions +graillonnates +graillonnerez +graissassions +graisseraient +grandissement +granitassions +graniteraient +grappilleront +graticulaient +graticulerons +gravellerions +gravillonnera +grenelassions +griffonnerais +grignotassent +grignotements +grillagerions +grinchassions +grisaillerais +grognasseriez +grognonnasses +grossissement +grouillassiez +grouillerions +gueuletonnons +guillemetames +guillochasses +guillocherons +guillotineurs +guinchassions +gynogenetique +habitueraient +haillonneuses +hameconnaient +handicaperiez +hannetonnasse +harmonicordes +harmonisasses +harmoniseriez +hectometrique +heliotropisme +helitreuillat +hellenisasses +helleniseriez +hemiparasites +hemipteroides +hemorroidales +heptathlonien +herbageassiez +herboriserons +heroicomiques +heterogreffes +heteroplastie +heterozygotie +hierarchisees +hippiatriques +hippotechnies +hispaniserent +historiassiez +holographiiez +homogeneifier +homologuerais +horizontalite +hortillonnage +humanisassiez +humaniserions +humidifiaient +humidifieriez +hydrocraqueur +hydrocuteriez +hydrofugeront +hydrogeneront +hydrolyserons +hydropulseurs +hypergoliques +hypernatremie +hyperthermies +hypnotherapie +hypnotiserent +hypnotiseuses +hypochlorites +hypothequates +hypothequeras +hypothequions +hysteriformes +ichneumonides +ichtyologique +iconoclasties +idealiserions +idiosyncrasie +idiotifiasses +idiotifierons +illegitimites +illusionnerai +immatriculais +immortalisees +immunisassiez +immuniserions +impartageable +impersonnelle +imperturbable +implantations +importatrices +importunerait +imprecatoires +imprevoyances +imprimabilite +improbatrices +improvisasses +improviserait +inaccordables +inactivassiez +inactiverions +inapplicables +inassimilable +inaugurations +incantatoires +incinerassiez +inconcevables +inconsistants +incorporalite +incorporasses +incorrections +incrustassiez +incrusterions +indefrichable +indemontrable +indianiseront +indifferasses +indifferences +indiffereriez +indirectement +indisposaient +individualisa +indulgenciant +inexplicables +inexpugnables +infantilisees +infatuassions +inferentielle +inferiorisees +infibulations +informatisiez +ingurgiterent +inhospitalite +initialassent +initialiserai +inopportunite +inqualifiable +insaisissable +insatisfaites +insecurisates +insecuriseras +insecurisions +insignifiants +insinuassions +installations +instillerions +institutrices +instruisisses +insubordonnee +insufflassent +insufflatrice +insurpassable +intaillassent +intaillerions +intelligentes +intentionnees +interafricain +interagissons +interastrales +intercederons +intercepteurs +interesserait +interfaceront +interferasses +interferentes +interiorisons +interjetteras +interloquasse +intermariages +intermittents +interoceptifs +interpellerai +interpenetrat +interpenetrez +interpolerais +interposerent +interpretasse +interrogeriez +interrompions +interromprons +interruptrice +intertribales +intervertiras +interviendrai +interviennent +interviewames +interviewerez +intervinssent +intimidassiez +introductions +inutilisation +invalidations +invectiverent +investiraient +irredentismes +irreprochable +islamologique +isochroniques +isomerisasses +isomeriseriez +isothermiques +italianisates +jaillissement +jargonnassent +javelliserons +jointoyassent +journalisates +journaliseras +journalisions +judiciarisant +judiciarisent +juponnassions +juponneraient +justificateur +juxtaposables +juxtaposasses +juxtaposerons +kaoliniseront +khartoumaises +kitesurfeuses +labellisaient +labelliserons +labialiseront +labiopalatale +labyrinthites +laicisassions +lambrissasses +lambrisserons +lapidifierais +laryngectomie +latifundistes +latinisassent +lechouillerai +lemmatisation +lemmatiserais +lepidosaurien +lesionnerions +leucocytaires +liberalisasse +ligaturassent +ligaturerions +lilliputienne +limogeassions +linearisation +lipschitziens +longitudinale +loquetassions +lubrificateur +lubrifierions +lusitaniennes +lycopodiacees +maastrichtien +macadamisasse +machurassions +machureraient +macrocytaires +macroglossies +maderisassent +magasinassent +magasinerions +magdalenienne +magnetiserait +magnetomoteur +magnetoscopes +mainmortables +maitrisassiez +malchanceuses +malendurantes +malveillantes +mammalogiques +manchonnasses +manchonnerons +mangeotassent +mangeoterions +mangeottaient +manifesterait +manigancerait +manucurassiez +maquereautais +maquerellerai +marabouterais +maraudassions +marauderaient +marchanderiez +marchandisant +marchandisent +marchandisons +marguillieres +marivaudaient +marivauderait +marmonnassiez +marmonnerions +marmoriserent +maroquinasses +maroquineriez +marquetterait +marronnassent +martensitique +martyrisasses +martyriserons +massicoteront +massification +materialisant +materialisent +materialistes +mathematisant +mathematisent +matriculerais +maugreeraient +meconnaitrons +medaillassent +medaillerions +mediatiserait +medicalisames +medicaliseras +medicalisions +medicamentera +megacephalies +megatonniques +melangeassiez +melanodermies +mendigoteront +mentaliseront +mentionnaient +mercantilisme +merceriserent +merovingienne +mesalliassiez +mesestimaient +mesestimerons +metalliserait +metalloidique +metallurgiste +metaphorisais +metastasaient +metatarsienne +meteoriserent +methacrylique +methaniserait +micaschisteux +microalveoles +microchimique +microfichages +microficherai +microhistoire +micrometrique +micronesienne +micronisation +microniserais +microscopique +millesimeriez +millimetrates +millimetreras +millimetrions +millionnaires +miniaturisant +miniaturisent +minimalisames +minimaliseras +minimalisions +missionnaires +mitigeassions +mitraillaient +mixtionnaient +modernisaient +moderniserent +moissonnasses +moissonnerons +molluscicides +monnayassions +monnayeraient +monolithismes +monologueront +monoparentaux +monophoniques +montalbanaise +montreuillois +morphogeneses +morphosyntaxe +mortification +motorisassiez +motoriserions +moucharderiez +moucheronnait +moufeteraient +moulinassions +moulineraient +mouvementasse +mucilagineuse +muguetterions +multinational +multiplexates +multiplexeras +multiplieront +multisoupapes +municipalisat +munitionnasse +mutualisation +mutualiserais +mycodermiques +myristicacees +mythomaniaque +nanomateriaux +nanophysiques +narcoleptique +naturalisames +naturaliseras +naturalisions +naufrageaient +nazifieraient +necessiterais +nectariniides +negationnisme +negligeassent +negocieraient +neocommuniste +neologiserent +nephrostomies +neurobiologie +neurochimique +neurotoxiques +nivoglaciaire +nivopluviales +nomadisations +nomenclatures +normalisaient +normaliserent +notifiassions +nouakchottois +nourrissantes +nucleariserez +nucleotidique +numerotassiez +numeroterions +nyctaginacees +objectiverait +obombrassions +obombreraient +obscurantisme +obscurcirions +obscurcissent +obsessionnels +obtemperaient +obtempererait +occultassions +occupationnel +odontalgiques +oeilletonnera +oleandomycine +oligoelements +oligophrenies +ombrageraient +opiniatrerons +opposabilites +optimaliserez +optimisassent +orchestrerait +ordinaticiens +ordonnancerai +organiquement +organisateurs +organiserions +organsinaient +orientalisent +orientalistes +orthopedistes +oscillatoires +oscillogramme +ostentatoires +osteomalacies +ostraciserait +outrecuidants +ouvrageraient +ovationneront +pacificateurs +pactisassions +pagnotassions +pagnoteraient +paillarderait +pailletterait +paillonnaient +paleochretien +palethnologie +palissadasses +palissaderons +palissonnasse +palmatisequee +paneuropeenne +pangermaniste +panophtalmies +panoramiquais +panoramiquent +pantheonisant +paracheveront +parachronisme +parachuterons +paraffinerent +paraguayennes +paralysassiez +paraphlebites +paraphrasasse +parasexualite +paratonnerres +parcellarisez +parcellisames +parcelliseras +parcellisions +parcheminerie +pardonnassiez +parementerent +parfondissiez +parisianisent +parisianistes +parkerisation +parkeriserais +parkinsoniens +parodontolyse +parquetteriez +parsemassions +parsemeraient +partageassiez +participantes +participeront +partisaneries +partouserions +passerillages +passionnistes +pastellassent +pastellerions +pasteuriserai +pastillassent +pastillerions +patouilleront +patrigoterais +patronnassent +patronnerions +patrouillerai +paumoieraient +pauperisaient +pauperiserons +pecloteraient +peinturassent +peinturerions +peinturlurons +pelletisation +pelotonnerais +penalisassiez +penaliserions +penitentielle +pensionnaires +pensionneront +pentadactyles +peopolisation +perceptuelles +perennisasses +perenniseriez +perfectionnas +pericliteront +periphrasames +periscolaires +perisplenites +peristaltique +permacultures +permanenciere +permeabilites +peroxydassent +perpetrations +perpetuations +perpetuerions +persecuterais +persevereront +persifflerent +persiffleuses +persillassent +perturbations +pervibrassent +petardassions +petarderaient +petrarquisiez +phallocraties +pharmaciennes +pharmacologue +pharyngoscope +phascolomides +philanthropes +philatelistes +philistinisme +philosophames +phlogistiques +phosphaturies +phosphorerent +photocatalyse +photocomposer +photocopiames +photocopierez +photoelectron +photolectures +photosynthese +phototherapie +phylloxerique +phytotoxicite +piapiatassiez +pictorialisme +pietonnisasse +piezometrique +pigmentassent +pigouillasses +pigouillerons +pinceauterais +piqueniquasse +piqueniquerai +pixellisation +pixelliserais +placardassiez +placardisates +placardiseras +placardisions +plancheierais +plaqueminiers +plasticulture +plastifieront +plastiquasses +plastronnates +plastronneras +plastronnions +platyrhiniens +pleuvasserait +ploutocraties +pluraliserent +pneumectomies +podcastassent +podcasterions +poetisassions +poignarderais +poinconnement +poinconnerent +poinconneuses +pointillerais +poireauterent +polemiquerent +polemoniacees +politiquasses +politiqueront +polyacrylique +polycondensat +polycopieriez +polygonations +polytechnique +polyvinylique +pomeraniennes +ponderabilite +portageraient +portefeuilles +portemonnaies +portionnaires +portlandienne +portraiturais +positionnerez +positionnions +posterisasses +posteriserons +postillonnons +postsonorisai +postsonorisee +postulassions +potentialisas +pouponnassent +pouponnerions +pourchasserai +pourlecherent +pourrissables +poursuivisses +preaffranchie +preavisassent +preaviserions +precambrienne +precarisaient +precariserons +precautionnas +preciseraient +precommandons +preconisaient +preconiserent +precontraints +predecesseure +predelinquant +predisposames +predominerais +prefabriquais +prefiguraient +prefigurerons +prefinancates +prefixassions +prefloraisons +preinscrirait +preinscrivons +prejudiciates +prejugeraient +preoccupantes +preoccupation +preoccuperais +preoperatoire +preromantique +presbyacousie +prescriptives +prescrivaient +presentielles +preservations +presonorisait +pressentiriez +pressurisasse +preteriteriez +pretintailles +previsibilite +priorisassiez +prioriserions +privatisation +privatiserais +privilegiasse +problematiser +procederaient +procreeraient +produisissiez +profanassions +professorales +profiteraient +programmerait +progressasses +progressistes +projetassions +proletarisait +prolifererait +prolongeaient +promulguasses +promulguerons +pronostiquais +pronostiquiez +propedeutique +propharmacien +propitiations +proprioceptif +proscriptions +prosencephale +prosperassent +prosternasses +prosternement +prosternerent +protegeraient +prothrombines +protohistoire +protoplanetes +prototypaient +providentiels +provignassiez +provignerions +provisionnons +provocatrices +psalmodieriez +psychasthenie +psychiquement +psychomotrice +pteridophytes +pulsionnelles +pulverisaient +pulveriseriez +purificatoire +putrefiassent +putrefierions +pyroclastique +pyrogalliques +pyrograveront +pyrolysassiez +quadrilleriez +quantifiables +quantifiasses +quantifierent +quebecisaient +questionnerai +quincailliere +quintoyassent +quintuplerent +raboudinerais +raccourcirons +raccoutumames +raccoutumerez +radicaliserai +radiobalisage +radiobalisiez +radiographies +radiotherapie +radoucissions +rafistoleront +raidissements +ralentissions +rallongeaient +randomisation +randomiserais +rapatriassent +rapatriements +rapatronnasse +rapetasserait +raplombassent +raplomberions +rappareillais +rappariassiez +rapparierions +rappointirais +rapportassiez +rassortissant +ratatinassiez +ratatouillait +ratiboiserais +ratiocinaient +raugmenterent +reabonnassiez +reabonnerions +reabsorbaient +reactivassent +reactualisait +readaptassiez +readapterions +readmettaient +readmettrions +reaffirmerent +reagencassent +reagencements +realimentates +realimenteras +realimentions +reappropriait +rearrangeront +reassignasses +reassigneriez +reattribuates +reattribueras +reattribuions +reblanchirent +rebobinassent +rebobinerions +rebrancherent +rebrulassions +rebruleraient +recachetasses +recalcifierez +recalibrerent +recapitalisas +recausassions +rechappassiez +rechargerions +rechauffaient +rechaufferons +rechignassent +rechignements +reciproquerez +recoiffassent +recoifferions +recombinasses +recombinerons +recommencates +recomparaitre +recomposables +recomposasses +recomposerons +recomptassent +recompterions +reconciliasse +reconcilieras +reconciliions +recondamnasse +reconduisimes +reconfigurees +reconfortates +reconforteras +reconfortions +reconnaitrons +reconnectasse +reconstituant +reconstruiras +reconstruisit +recontactates +recontacteras +recontactions +reconvertimes +reconvoquames +reconvoquerez +recorrigeasse +recriminateur +rectifiassent +rectification +rectifierions +recueillerait +recueillisses +reculotteriez +recuperateurs +redecouperais +redefinissant +redemanderiez +redeploierais +redescendites +redescendriez +redessineront +rediffusaient +redigeassions +rediscuteront +redistribuiez +reduplicative +redynamiserez +reedifiassent +reeligibilite +reemploieriez +reengagements +reenregistrez +reensemencant +reensemencent +reentendirent +reescomptames +reescompterez +reessaierions +reexecutaient +refaconneront +refacturaient +referencerait +refermenterez +refinancaient +refinancerons +refondatrices +reformulerais +refouillerait +refourguerait +refrigeration +refrigererais +refroidissait +regazonnerent +regeneratives +regionalisiez +regreffassent +regrefferions +regressassent +regularisames +regulariseras +regularisions +rehabiliterai +reimplantasse +reimprimeront +reincarcerait +reinitialisai +reinitialisee +reinscrivites +reintegreront +reinterpreter +reintroduisez +reinvestirait +reinvitassent +reinviterions +rejointoierai +rejouissantes +relargiraient +relargissions +relationnelle +releguassions +relegueraient +relocalisasse +reluisissions +remachassions +remacheraient +remarchassiez +rembarquement +rembarquerent +rembarrassent +rembarrerions +rembauchaient +rembourrasses +rembourrerons +rembourserais +rememorations +remilitariser +remmaillerais +remmanchasses +remmancherons +remobilisates +remorquassiez +rempaquettera +rempoissonnes +rencaisserais +renchaussasse +rencherissons +rencontrasses +rencontrerons +rendormissent +rendossassiez +renegocierent +renflammerais +renflouassiez +renflouerions +renforcassiez +renforcements +renforcissant +renformissais +renfrognerait +rengorgeasses +renseignement +renseignerent +rentabilisant +rentabilisent +rentrairaient +renumerotames +renumeroterez +reordonnerent +reouvriraient +reoxygeneront +repaireraient +reparlassions +reparleraient +repeignissent +repentissions +repercuterait +repositionnez +reprecisaient +representable +representants +representerai +reprogrammera +requinquerais +requisitoires +resituassions +resitueraient +resocialisees +respiratoires +resplendirait +resquillaient +ressoudassent +ressouderions +ressourcaient +ressourceriez +ressurgissons +restructuriez +resulteraient +resurchauffat +retapisseriez +reteignissent +retelephonant +retorquassiez +retranchasses +retrancheriez +retransmettre +retraverserai +retrempassent +retremperions +retrocederont +retrospectifs +retrousseront +retrouvassent +retrouverions +reunification +reunionnaises +revalorisames +revaloriseras +revalorisions +revassassions +reveillonnant +reveillonneur +reverbereront +reverencieuse +reverifiaient +revisitassiez +reviviscences +revolverisera +revoquassions +revoqueraient +rhetoriqueurs +rhinoplasties +riboulassions +ribouleraient +rickettsioses +ridiculiserai +rigidifiaient +ripaillassent +ristournerait +ritualisaient +ritualiserons +robinetteries +roboticiennes +robotisations +rognonnassiez +romanisassent +ronchonnasses diff --git a/backend/eurydice/common/cleaning/__init__.py b/backend/eurydice/common/cleaning/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/common/cleaning/repeated_task.py b/backend/eurydice/common/cleaning/repeated_task.py new file mode 100644 index 0000000..b85dc0f --- /dev/null +++ b/backend/eurydice/common/cleaning/repeated_task.py @@ -0,0 +1,79 @@ +import abc +import datetime +import time +from typing import Optional + +from django.db import connections +from django.utils import timezone + +from eurydice.common.utils import signals + +ONE_SECOND_TIMEDELTA = datetime.timedelta(seconds=1) + + +class RepeatedTask(abc.ABC): + """An abstract task that runs indefinitely until signal interruption (SIGINT). + Inherit this class and implement the `_run` and `_ready` methods with your own + implementation. + """ + + def __init__( + self, + run_every: datetime.timedelta, + check_if_should_run_every: datetime.timedelta = ONE_SECOND_TIMEDELTA, + ): + """ + Args: + run_every: minimal timedelta between each run. Any call to `_run` beyond + the first call is guaranteed to happen `run_every` after the previous + call. + check_if_should_run_every: delay between checks that trigger a run if the + `run_every` timedelta has passed. Defaults to 1 second. + """ + self._last_run_at: Optional[datetime.datetime] = None + self._run_every = run_every + self._check_if_should_run_every = check_if_should_run_every.total_seconds() + + @abc.abstractmethod + def _ready(self) -> None: + """Called when RepeatedTask is ready, just before first loop. This method is meant + to be overridden, for example to log that the task is ready to loop. + Raises NotImplementedError by default. Override this in your own class. + """ + raise NotImplementedError + + def _should_run(self) -> bool: + """Determine whether the cleaning task should be run based on the last run of the + task and the check_if_should_run_every parameter. + """ + if self._last_run_at is None: + return True + + return timezone.now() >= self._last_run_at + self._run_every + + @abc.abstractmethod + def _run(self) -> None: + """The function to be called each loop, when `_should_run` returns True. + Raises NotImplementedError by default. Override this in your own class. + """ + raise NotImplementedError + + def _loop(self) -> None: + """Loop indefinitely until interrupted, and call `_run` at a given frequency.""" + keep_running = signals.BooleanCondition() + + self._ready() + + while keep_running: + if self._should_run(): + self._run() + self._last_run_at = timezone.now() + + time.sleep(self._check_if_should_run_every) + + def start(self) -> None: # pragma: no cover + """Entrypoint for the RepeatedTask.""" + try: + self._loop() + finally: + connections.close_all() diff --git a/backend/eurydice/common/config/__init__.py b/backend/eurydice/common/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/common/config/gunicorn.conf.py b/backend/eurydice/common/config/gunicorn.conf.py new file mode 100644 index 0000000..f0f45f4 --- /dev/null +++ b/backend/eurydice/common/config/gunicorn.conf.py @@ -0,0 +1,8 @@ +bind = "0.0.0.0:8080" +timeout = 30 +worker_class = "gthread" +worker_connections = 1000 +workers = 2 +threads = 4 +loglevel = "error" +accesslog = "-" diff --git a/backend/eurydice/common/config/settings/__init__.py b/backend/eurydice/common/config/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/common/config/settings/base.py b/backend/eurydice/common/config/settings/base.py new file mode 100644 index 0000000..3638951 --- /dev/null +++ b/backend/eurydice/common/config/settings/base.py @@ -0,0 +1,409 @@ +import pathlib +from os import path + +import django.core.exceptions +import environ +import humanfriendly +from django.utils.translation import gettext_lazy as _ + +env = environ.Env( + DEBUG=(bool, False), + ALLOWED_HOSTS=(list, ["localhost", "127.0.0.1", "api"]), + CSRF_TRUSTED_ORIGINS=(list, []), + SECRET_KEY=(str, "thxdd#i*^!nt-1s_md@q-&"), + MINIO_ENABLED=(bool, True), + MINIO_SECURE=(bool, False), + MINIO_BUCKET_NAME=(str, "eurydice"), + MINIO_ENDPOINT=(str, ""), + MINIO_ACCESS_KEY=(str, ""), + MINIO_SECRET_KEY=(str, ""), + TRANSFERABLE_STORAGE_DIR=(str, "/home/eurydice/data"), + # This value should be kept in sync with the TRANSFERABLE_MAX_SIZE + # constant defined in the frontend/src/origin/constants.js file + TRANSFERABLE_MAX_SIZE=(str, "50TiB"), + LOG_FORMAT=(str, "%(asctime)s - %(name)s - %(levelname)s - %(message)s"), + LOG_LEVEL=(str, "INFO"), + LOG_TO_FILE=(bool, False), + USER_ASSOCIATION_TOKEN_EXPIRES_AFTER=(str, "15min"), + METRICS_SLIDING_WINDOW=(str, "60min"), + REMOTE_USER_HEADER_AUTHENTICATION_ENABLED=(bool, False), + REMOTE_USER_HEADER=(str, "HTTP_X_REMOTE_USER"), + EURYDICE_VERSION=(str, "development"), + SECURE_COOKIES=(bool, True), + SAMESITE_COOKIES=(str, "Strict"), + SESSION_COOKIE_AGE=(str, "24h"), + PAGE_SIZE=(int, 50), + MAX_PAGE_SIZE=(int, 100), + THROTTLE_RATE=(str, "30/second"), + EURYDICE_API=(str, ""), + EURYDICE_CONTACT=(str, ""), + EURYDICE_CONTACT_FR=(str, ""), + UI_BADGE_CONTENT=(str, "default badge value"), + UI_BADGE_COLOR=(str, "black"), +) + +DEBUG = env("DEBUG") + +BASE_DIR = path.dirname( + path.dirname(path.dirname(path.dirname(path.abspath(__file__)))) +) + +COMMON_DOCS_PATH = pathlib.Path(BASE_DIR) / "common" / "api" / "docs" / "static" + +SECRET_KEY = env("SECRET_KEY") + +ALLOWED_HOSTS = env("ALLOWED_HOSTS") +CSRF_TRUSTED_ORIGINS = env("CSRF_TRUSTED_ORIGINS") + +# The version number for this release of the eurydice application +EURYDICE_VERSION = env("EURYDICE_VERSION") + +# Contact information for support +EURYDICE_CONTACT = env("EURYDICE_CONTACT") + +# https://docs.djangoproject.com/fr/3.2/ref/settings/#append-slash +APPEND_SLASH = True + +# Tell django to trust X-Forwarded-Proto header sent by reverse proxy to determine user's protocol +# https://docs.djangoproject.com/fr/3.2/ref/settings/#secure-proxy-ssl-header +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + +INSTALLED_APPS = [ + "whitenoise.runserver_nostatic", + "django.contrib.staticfiles", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "rest_framework", + "rest_framework.authtoken", + "django_filters", + "drf_spectacular", + "eurydice.common.redoc.apps.ReDocConfig", + "eurydice.common.permissions.apps.PermissionsConfig", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "whitenoise.middleware.WhiteNoiseMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "eurydice.common.api.middlewares.AuthenticatedUserHeaderMiddleware", + "eurydice.common.api.middlewares.LastAccessMiddleware", +] + +AUTHENTICATION_BACKENDS = ["django.contrib.auth.backends.ModelBackend"] + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [ + path.join(BASE_DIR, "templates"), + ], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ] + }, + } +] + +OPTIONS = {"context_processors"} + +# Session configuration +# https://docs.djangoproject.com/en/3.2/ref/settings/#id15 + +# https://docs.djangoproject.com/en/3.2/ref/settings/#session-cookie-age +SESSION_COOKIE_AGE = humanfriendly.parse_timespan(env("SESSION_COOKIE_AGE")) + +# https://docs.djangoproject.com/fr/3.2/ref/settings/#session-cookie-secure +SESSION_COOKIE_SECURE = env("SECURE_COOKIES") + +# https://docs.djangoproject.com/en/3.2/ref/settings/#session-cookie-samesite +SESSION_COOKIE_SAMESITE = env("SAMESITE_COOKIES") + +# https://docs.djangoproject.com/en/3.2/ref/settings/#session-cookie-name +SESSION_COOKIE_NAME = "eurydice_sessionid" + +# CSRF configuration + +# https://docs.djangoproject.com/en/3.2/ref/settings/#csrf-cookie-name +CSRF_COOKIE_NAME = "eurydice_csrftoken" + +# https://docs.djangoproject.com/en/3.2/ref/settings/#csrf-cookie-samesite +CSRF_COOKIE_SAMESITE = env("SAMESITE_COOKIES") + +# https://docs.djangoproject.com/en/3.2/ref/settings/#csrf-cookie-secure +CSRF_COOKIE_SECURE = env("SECURE_COOKIES") + +# Pagination + +PAGE_SIZE = env("PAGE_SIZE") +MAX_PAGE_SIZE = env("MAX_PAGE_SIZE") + +# Throttling + +THROTTLE_RATE = env("THROTTLE_RATE") + +# DRF + +REST_FRAMEWORK = { + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": PAGE_SIZE, + "DEFAULT_RENDERER_CLASSES": ["rest_framework.renderers.JSONRenderer"], + "DEFAULT_PARSER_CLASSES": [ + "rest_framework.parsers.JSONParser", + ], + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.TokenAuthentication", + ], + "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"], + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", + "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"], + "DEFAULT_THROTTLE_RATES": {"create_transferable": THROTTLE_RATE}, +} + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": env("DB_NAME"), + "USER": env("DB_USER"), + "PASSWORD": env("DB_PASSWORD"), + "HOST": env("DB_HOST"), + "PORT": env("DB_PORT"), + "ATOMIC_REQUESTS": True, + } +} + +# Password validation +# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa: E501 + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + +# Remote authentication +# https://docs.djangoproject.com/en/3.2/howto/auth-remote-user/ +# https://www.django-rest-framework.org/api-guide/authentication/#remoteuserauthentication + +REMOTE_USER_HEADER_AUTHENTICATION_ENABLED = env( + "REMOTE_USER_HEADER_AUTHENTICATION_ENABLED" +) + +REMOTE_USER_HEADER = env("REMOTE_USER_HEADER") + +if REMOTE_USER_HEADER_AUTHENTICATION_ENABLED: + AUTHENTICATION_BACKENDS.append("django.contrib.auth.backends.RemoteUserBackend") + REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"].append( + "rest_framework.authentication.SessionAuthentication" + ) +else: + # BasicAuthentication needs to be on top so that django replies with + # `WWW-Authenticate: Basic` instead of `WWW-Authenticate: Token` + # along with a 401 + REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"].insert( + 0, "rest_framework.authentication.BasicAuthentication" + ) + + +# Internationalization +# https://docs.djangoproject.com/en/3.2/topics/i18n/ + +LANGUAGE_CODE = "fr-FR" + +TIME_ZONE = "Europe/Paris" + +USE_I18N = False + +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/3.2/howto/static-files/ + +STATIC_URL = "/static/" +STATIC_ROOT = path.join(BASE_DIR, "staticfiles") + +STORAGES = { + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + } +} + +# Logging +# https://docs.djangoproject.com/en/3.2/topics/logging/ + +LOGGING_EURYDICE_HANDLERS = [ + "default", +] +LOG_TO_FILE = env("LOG_TO_FILE") +if LOG_TO_FILE: + LOGGING_EURYDICE_HANDLERS.append("file") + +LOGGING = { + "disable_existing_loggers": False, + "filters": { + "require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}, + "require_debug_true": {"()": "django.utils.log.RequireDebugTrue"}, + }, + "formatters": { + "default": {"format": env("LOG_FORMAT")}, + "json": { + "class": "eurydice.common.logging.JSONFormatter", + }, + }, + "handlers": { + "default": { + "level": "DEBUG", + "formatter": "default", + "class": "logging.StreamHandler", + }, + # django debug logs are only logged when DEBUG is True + "django_console": { + "class": "logging.StreamHandler", + "formatter": "default", + "filters": ["require_debug_true"], + "level": "DEBUG", + }, + # django requests are always logged + "django.server": { + "class": "logging.StreamHandler", + "formatter": "default", + "level": "DEBUG", + }, + }, + "loggers": { + "django": { + "handlers": LOGGING_EURYDICE_HANDLERS, + "level": env("LOG_LEVEL"), + }, + # django requests log + "django.server": { + "handlers": LOGGING_EURYDICE_HANDLERS, + "level": env("LOG_LEVEL"), + "propagate": False, + }, + "eurydice": { + "handlers": LOGGING_EURYDICE_HANDLERS, + "level": env("LOG_LEVEL"), + }, + }, + "version": 1, +} + +if LOG_TO_FILE: + LOGGING["handlers"]["file"] = { + "class": "logging.handlers.RotatingFileHandler", + "filename": pathlib.Path("/") / "var" / "log" / "app" / "log.json", + "formatter": "json", + "maxBytes": 1024 * 1024 * 15, # 15MB + "backupCount": 10, + "level": "DEBUG", + } + +# Minio + +MINIO_ENDPOINT = env.str("MINIO_ENDPOINT") +MINIO_ACCESS_KEY = env.str("MINIO_ACCESS_KEY") +MINIO_SECRET_KEY = env.str("MINIO_SECRET_KEY") +MINIO_SECURE = env("MINIO_SECURE") +MINIO_BUCKET_NAME = env("MINIO_BUCKET_NAME") +MINIO_ENABLED = env("MINIO_ENABLED") +# Following setting is overridden in destination and origin settings +MINIO_EXPIRATION_DAYS = 9 + +# If minio is disabled, use filesystem +TRANSFERABLE_STORAGE_DIR = env.str("TRANSFERABLE_STORAGE_DIR") + +if MINIO_ENABLED: + if not MINIO_ENDPOINT: + raise django.core.exceptions.ImproperlyConfigured( + "The MINIO_ENDPOINT environment variable must not be empty" + ) +else: + if not TRANSFERABLE_STORAGE_DIR: + raise django.core.exceptions.ImproperlyConfigured( + "The TRANSFERABLE_STORAGE_DIR environment variable must not be empty" + ) + +# drf-spectacular +# https://drf-spectacular.readthedocs.io/ + +SPECTACULAR_SETTINGS = { + "POSTPROCESSING_HOOKS": [ + "eurydice.common.api.docs.custom_spectacular.postprocessing_hook" + ], + "VERSION": "v1", + "APPEND_COMPONENTS": { + "securitySchemes": {}, + }, + "TAGS": [ + { + "name": "OpenApi3 documentation", + "description": _((COMMON_DOCS_PATH / "openapi.md").read_text()), + }, + { + "name": "Transferring files", + "description": _((COMMON_DOCS_PATH / "transferring-files.md").read_text()), + }, + { + "name": "Account management", + "description": _((COMMON_DOCS_PATH / "account-management.md").read_text()), + }, + ], +} + + +SPECTACULAR_SETTINGS["DESCRIPTION"] = ( + (COMMON_DOCS_PATH / "welcome.md") + .read_text() + .format( + EURYDICE_API=env("EURYDICE_API").upper(), + EURYDICE_HOST=ALLOWED_HOSTS[0], + EURYDICE_VERSION=EURYDICE_VERSION, + EURYDICE_CONTACT=EURYDICE_CONTACT, + ) +) + +# Eurydice + +# The maximum size in bytes of a Transferable i.e. a file submitted to be transferred. +# The limit value of this parameter is set by Minio +# https://docs.min.io/docs/minio-server-limits-per-tenant.html. +TRANSFERABLE_MAX_SIZE = humanfriendly.parse_size( + env("TRANSFERABLE_MAX_SIZE"), binary=False +) + +METADATA_HEADER_PREFIX = "Metadata-" + +USER_ASSOCIATION_TOKEN_SECRET_KEY = env.str("USER_ASSOCIATION_TOKEN_SECRET_KEY") +USER_ASSOCIATION_TOKEN_EXPIRES_AFTER = humanfriendly.parse_timespan( + env("USER_ASSOCIATION_TOKEN_EXPIRES_AFTER") +) + +METRICS_SLIDING_WINDOW = humanfriendly.parse_timespan(env("METRICS_SLIDING_WINDOW")) + +EURYDICE_CONTACT_FR = env("EURYDICE_CONTACT_FR") + +UI_BADGE_CONTENT = env("UI_BADGE_CONTENT") +UI_BADGE_COLOR = env("UI_BADGE_COLOR") diff --git a/backend/eurydice/common/config/settings/dev.py b/backend/eurydice/common/config/settings/dev.py new file mode 100644 index 0000000..e11036c --- /dev/null +++ b/backend/eurydice/common/config/settings/dev.py @@ -0,0 +1,9 @@ +from eurydice.common.config.settings.base import * + +DEBUG = True + +env = environ.Env( + FAKER_SEED=(int, 9835234), +) + +FAKER_SEED = env("FAKER_SEED") diff --git a/backend/eurydice/common/config/settings/test.py b/backend/eurydice/common/config/settings/test.py new file mode 100644 index 0000000..42a2dca --- /dev/null +++ b/backend/eurydice/common/config/settings/test.py @@ -0,0 +1,25 @@ +import environ + +from eurydice.common.config.settings.base import * +from eurydice.common.config.settings.dev import FAKER_SEED + +REMOTE_USER_HEADER_AUTHENTICATION_ENABLED = True + +if "django.contrib.auth.backends.RemoteUserBackend" not in AUTHENTICATION_BACKENDS: + AUTHENTICATION_BACKENDS.append("django.contrib.auth.backends.RemoteUserBackend") + +if ( + "rest_framework.authentication.SessionAuthentication" + not in REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] +): + REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"].append( + "rest_framework.authentication.SessionAuthentication" + ) + +REST_FRAMEWORK["TEST_REQUEST_DEFAULT_FORMAT"] = "json" + +USER_ASSOCIATION_TOKEN_SECRET_KEY = "" # nosec + +EURYDICE_CONTACT_FR = "contact fr" +UI_BADGE_CONTENT = "ui badge content" +UI_BADGE_COLOR = "ui badge color" diff --git a/backend/eurydice/common/enums.py b/backend/eurydice/common/enums.py new file mode 100644 index 0000000..2d69cf4 --- /dev/null +++ b/backend/eurydice/common/enums.py @@ -0,0 +1,56 @@ +"""Enums used both on the origin and destination sides.""" + +from typing import Set + +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class OutgoingTransferableState(models.TextChoices): + """The set of all possible "global" states for an OutgoingTransferable.""" + + PENDING = "PENDING", _("Pending") + ONGOING = "ONGOING", _("Ongoing") + ERROR = "ERROR", _("Error") + CANCELED = "CANCELED", _("Canceled") + SUCCESS = "SUCCESS", _("Success") + + @classmethod + def get_final_states(cls) -> Set["OutgoingTransferableState"]: + """List final states. + + Returns: a set containing the final states. + + """ + return {cls.ERROR, cls.CANCELED, cls.SUCCESS} # pytype: disable=bad-return-type + + @property + def is_final(self) -> bool: + """Check that the state is terminal. + + Returns: True if the state is final, False otherwise. + + """ + return self in self.get_final_states() + + +class TransferableRevocationReason(models.TextChoices): + """The set of all possible revocation reasons for an a TransferableRevocation.""" + + USER_CANCELED = "USER_CANCELED", _("Canceled by the user") + UPLOAD_SIZE_MISMATCH = "UPLOAD_SIZE_MISMATCH", _( + "The size of the uploaded Transferable did " + "not match the size given in the Content-Length header." + ) + OBJECT_STORAGE_FULL = "OBJECT_STORAGE_FULL", _( + "No more space on API object storage" + ) + UNEXPECTED_EXCEPTION = "UNEXPECTED_EXCEPTION", _( + "Unexpected error occurred while handling Transferable" + ) + + +__all__ = ( + "OutgoingTransferableState", + "TransferableRevocationReason", +) diff --git a/backend/eurydice/common/exceptions.py b/backend/eurydice/common/exceptions.py new file mode 100644 index 0000000..4943632 --- /dev/null +++ b/backend/eurydice/common/exceptions.py @@ -0,0 +1,7 @@ +class EurydiceError(Exception): + """Base class for Eurydice custom exceptions.""" + + +class S3ObjectNotFoundError(EurydiceError): + """Error raised when attempted to get from S3 storage an object that + doesn't exist.""" diff --git a/backend/eurydice/common/logging/__init__.py b/backend/eurydice/common/logging/__init__.py new file mode 100644 index 0000000..751ee6d --- /dev/null +++ b/backend/eurydice/common/logging/__init__.py @@ -0,0 +1,3 @@ +from .formatters import JSONFormatter + +__all__ = ["JSONFormatter"] diff --git a/backend/eurydice/common/logging/formatters.py b/backend/eurydice/common/logging/formatters.py new file mode 100644 index 0000000..eab1ecf --- /dev/null +++ b/backend/eurydice/common/logging/formatters.py @@ -0,0 +1,52 @@ +import json +import logging +from typing import Any +from typing import Union + +from django.http import HttpRequest + + +def json_django_http_request(obj: HttpRequest) -> dict: + """Serializes a Django HttpRequest into a smaller dict.""" + + as_dict = obj.__dict__ + + return { + "META": { + k: v for k, v in as_dict.get("META", {}).items() if not k.startswith("wsgi") + }, + "user": as_dict.get("user"), + "COOKIES": as_dict.get("COOKIES"), + } + + +def json_default(obj: Any) -> Union[str, dict]: + """Serializes an object into a dict, or a string representation of the object.""" + if isinstance(obj, HttpRequest): + return json_django_http_request(obj) + + return str(obj) + + +class JSONFormatter(logging.Formatter): + """Formats LogRecords into json-formatted strings.""" + + def format(self, record: logging.LogRecord) -> str: + """Serializes a log record into a json string""" + return json.dumps(self._record_to_dict(record), default=json_default) + + def _record_to_dict(self, record: logging.LogRecord) -> dict: + """Formats a log record into a dict""" + unwanted_elements = {"processName", "threadName"} + + as_dict = { + key: value + for key, value in record.__dict__.items() + if key not in unwanted_elements + } + + if "message" not in as_dict: + args = as_dict["args"] or {} + as_dict["message"] = as_dict["msg"] % args + + return as_dict diff --git a/backend/eurydice/common/minio.py b/backend/eurydice/common/minio.py new file mode 100644 index 0000000..64f6200 --- /dev/null +++ b/backend/eurydice/common/minio.py @@ -0,0 +1,11 @@ +from django.conf import settings +from minio import Minio + +client = Minio( + settings.MINIO_ENDPOINT, + access_key=settings.MINIO_ACCESS_KEY, + secret_key=settings.MINIO_SECRET_KEY, + secure=settings.MINIO_SECURE, +) + +__all__ = ("client",) diff --git a/backend/eurydice/common/models/__init__.py b/backend/eurydice/common/models/__init__.py new file mode 100644 index 0000000..e7d864b --- /dev/null +++ b/backend/eurydice/common/models/__init__.py @@ -0,0 +1,25 @@ +"""Models common between origin and destination.""" + +from .base import AbstractBaseModel +from .base import SingletonModel +from .base import TimestampSingleton +from .fields import S3BucketNameField +from .fields import S3ObjectNameField +from .fields import SHA1Field +from .fields import TransferableNameField +from .fields import TransferableSizeField +from .fields import UserProvidedMetaField +from .user import AbstractUser + +__all__ = ( + "AbstractBaseModel", + "AbstractUser", + "SingletonModel", + "TimestampSingleton", + "TransferableNameField", + "TransferableSizeField", + "SHA1Field", + "S3BucketNameField", + "S3ObjectNameField", + "UserProvidedMetaField", +) diff --git a/backend/eurydice/common/models/base.py b/backend/eurydice/common/models/base.py new file mode 100644 index 0000000..9d8effe --- /dev/null +++ b/backend/eurydice/common/models/base.py @@ -0,0 +1,63 @@ +import uuid +from datetime import datetime +from typing import Optional + +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class AbstractBaseModel(models.Model): + """ + Model using an UUID4 as primary key and + with automatic created_at field. + """ + + id = models.UUIDField( # noqa: VNE003 + primary_key=True, default=uuid.uuid4, editable=False + ) + + created_at = models.DateTimeField(auto_now_add=True, help_text=_("Creation date")) + + class Meta: + abstract = True + ordering = ("-created_at",) + + +class SingletonModel(models.Model): + """Model which can have only one instance persisted in the database.""" + + id = models.UUIDField( # noqa: VNE003 + primary_key=True, default=uuid.uuid4, editable=False + ) + _singleton = models.BooleanField(default=True, editable=False, unique=True) + + class Meta: + abstract = True + + +class TimestampSingleton(SingletonModel): + """Singleton holding a timestamp.""" + + timestamp = models.DateTimeField(auto_now=True) + + @classmethod + def update(cls) -> None: + """Update timestamp to current time.""" + cls.objects.update_or_create() + + @classmethod + def get_timestamp(cls) -> Optional[datetime]: + """Get the timestamp of the last received packet. + + Returns None if no packet has ever been received. + """ + try: + return cls.objects.get().timestamp + except cls.DoesNotExist: + return None + + class Meta: + abstract = True + + +__all__ = ("AbstractBaseModel", "SingletonModel", "TimestampSingleton") diff --git a/backend/eurydice/common/models/fields.py b/backend/eurydice/common/models/fields.py new file mode 100644 index 0000000..6e77334 --- /dev/null +++ b/backend/eurydice/common/models/fields.py @@ -0,0 +1,133 @@ +import collections.abc +from typing import Any +from typing import Dict + +from django.conf import settings +from django.core import exceptions +from django.core import validators +from django.db import models + + +class TransferableNameField(models.CharField): + """A field to store the name of a Transferable.""" + + MIN_LENGTH: int = 1 + MAX_LENGTH: int = 255 + + def __init__(self, *args, **kwargs) -> None: + kwargs["validators"] = ( + validators.MinLengthValidator(TransferableNameField.MIN_LENGTH), + ) + kwargs["max_length"] = TransferableNameField.MAX_LENGTH + super().__init__(*args, **kwargs) + + +class TransferableSizeField(models.PositiveBigIntegerField): + """A field to store the size in bytes of a Transferable.""" + + def __init__(self, *args, **kwargs) -> None: + kwargs["validators"] = ( + validators.MaxValueValidator(settings.TRANSFERABLE_MAX_SIZE), + ) + super().__init__(*args, **kwargs) + + +class SHA1Field(models.BinaryField): + """A field to store the SHA-1 of a file in a binary format.""" + + DIGEST_SIZE_IN_BYTES: int = 20 + + def __init__(self, *args, **kwargs) -> None: + kwargs["validators"] = ( + validators.MinLengthValidator(SHA1Field.DIGEST_SIZE_IN_BYTES), + ) + kwargs["max_length"] = SHA1Field.DIGEST_SIZE_IN_BYTES + super().__init__(*args, **kwargs) + + +class S3BucketNameField(models.CharField): + """A field to store the name of a S3 bucket.""" + + MIN_LENGTH: int = 3 + MAX_LENGTH: int = 63 + + def __init__(self, *args, **kwargs) -> None: + # bucket name restrictions + # https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-s3-bucket-naming-requirements.html + kwargs["validators"] = ( + validators.MinLengthValidator(S3BucketNameField.MIN_LENGTH), + ) + kwargs["max_length"] = S3BucketNameField.MAX_LENGTH + super().__init__(*args, **kwargs) + + +class S3ObjectNameField(models.CharField): + """A field to store the name of a S3 object located in a S3 bucket.""" + + MIN_LENGTH: int = 1 + MAX_LENGTH: int = 255 + + def __init__(self, *args, **kwargs) -> None: + kwargs["validators"] = ( + validators.MinLengthValidator(S3ObjectNameField.MIN_LENGTH), + ) + # Linux maximum filename length is 255 characters + kwargs["max_length"] = S3ObjectNameField.MAX_LENGTH + super().__init__(*args, **kwargs) + + +class UserProvidedMetaField(models.JSONField): + """A field to store Transferable metadata provided by the user as JSON.""" + + def validate(self, field_value: Any, model_instance: models.Model) -> None: + """ + Validate that the field contains a mapping of str to str with keys + starting with settings.METADATA_HEADER_PREFIX, and that the keys are + case insensitive. + """ + + super().validate(field_value, model_instance) + self._validate_is_mapping(field_value) + + for key, value in field_value.items(): + self._validate_mapping_key(key) + self._validate_mapping_value(value) + + self._validate_unique_lowercase_mapping_keys(field_value) + + def _validate_is_mapping(self, field_value: Any) -> None: + if not isinstance(field_value, collections.abc.Mapping): + raise exceptions.ValidationError("Value must be a mapping.") + + def _validate_mapping_key(self, key: Any) -> None: + if not isinstance(key, str): + raise exceptions.ValidationError("Keys of the mapping must be strings.") + + if not key.startswith(settings.METADATA_HEADER_PREFIX): + raise exceptions.ValidationError( + f"Metadata item names must start with " + f"{settings.METADATA_HEADER_PREFIX}" + ) + + def _validate_mapping_value(self, value: Any) -> None: + if not isinstance(value, str): + raise exceptions.ValidationError("Metadata item contents must be strings.") + + def _validate_unique_lowercase_mapping_keys( + self, field_value: Dict[str, str] + ) -> None: + lowercase_keys = [k.lower() for k in field_value.keys()] + if len(set(lowercase_keys)) != len(lowercase_keys): + raise exceptions.ValidationError( + "Metadata item names are case insensitive and must not be duplicated." + ) + + +__all__ = ( + "TransferableNameField", + "TransferableSizeField", + "SHA1Field", + "S3BucketNameField", + "S3ObjectNameField", + "UserProvidedMetaField", +) diff --git a/backend/eurydice/common/models/user.py b/backend/eurydice/common/models/user.py new file mode 100644 index 0000000..6e3de42 --- /dev/null +++ b/backend/eurydice/common/models/user.py @@ -0,0 +1,29 @@ +import uuid + +import django.contrib.auth.models +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class AbstractUser(django.contrib.auth.models.AbstractUser): + """A custom abstract user model using an UUID4 as primary key.""" + + id = models.UUIDField( # noqa: VNE003 + primary_key=True, default=uuid.uuid4, editable=False + ) + + last_access = models.DateTimeField( + _("last access"), + help_text=( + "Unlike last_login, this field gets updated every time the " + "user accesses the API, even when they authenticate with an API token." + ), + blank=True, + null=True, + ) + + class Meta: + abstract = True + + +__all__ = ("AbstractUser",) diff --git a/backend/eurydice/common/permissions/__init__.py b/backend/eurydice/common/permissions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/common/permissions/apps.py b/backend/eurydice/common/permissions/apps.py new file mode 100644 index 0000000..effc78a --- /dev/null +++ b/backend/eurydice/common/permissions/apps.py @@ -0,0 +1,7 @@ +from django import apps + + +class PermissionsConfig(apps.AppConfig): + name = "eurydice.common.permissions" + label = "eurydice_common_permissions" + default_auto_field = "django.db.models.AutoField" diff --git a/backend/eurydice/common/permissions/migrations/0001_initial.py b/backend/eurydice/common/permissions/migrations/0001_initial.py new file mode 100644 index 0000000..1e7e151 --- /dev/null +++ b/backend/eurydice/common/permissions/migrations/0001_initial.py @@ -0,0 +1,30 @@ +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="MonitoringPermissions", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ], + options={ + "permissions": (("view_rolling_metrics", "Can view rolling metrics"),), + "managed": False, + "default_permissions": (), + }, + ), + ] diff --git a/backend/eurydice/common/permissions/migrations/__init__.py b/backend/eurydice/common/permissions/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/common/permissions/models.py b/backend/eurydice/common/permissions/models.py new file mode 100644 index 0000000..b0bec8a --- /dev/null +++ b/backend/eurydice/common/permissions/models.py @@ -0,0 +1,14 @@ +from django.db import models + + +class MonitoringPermissions(models.Model): + """An unmanaged model that holds monitoring permissions users possess.""" + + class Meta: + managed = False # no associated database table + default_permissions = () # disable default permissions ("add", "change"...) + + permissions = (("view_rolling_metrics", "Can view rolling metrics"),) + + +__all__ = ("MonitoringPermissions",) diff --git a/backend/eurydice/common/protocol.py b/backend/eurydice/common/protocol.py new file mode 100644 index 0000000..871d184 --- /dev/null +++ b/backend/eurydice/common/protocol.py @@ -0,0 +1,224 @@ +"""This modules defines serializable objects used to communicate between the origin +and destination sides. +""" + +import uuid +from typing import Any +from typing import Dict +from typing import List +from typing import Optional + +import msgpack +import pydantic + +from eurydice.common import enums + + +class Transferable(pydantic.BaseModel): + """A Transferable sent as part of an OnTheWirePacket. + + Attributes: + id: a UUID identifying the Transferable on the origin and destination side. + name: the name of the file corresponding to the Transferable. + user_profile_id: the UUID of the user profile owning the Transferable on the + origin side. + user_provided_meta: the metadata provided by the user on file submission. + sha1: the SHA-1 digest of the file corresponding to the Transferable. + This attribute can be None as the digest of the Transferable is only + provided if the TransferableRange that refers to the object is the last. + size: the size in bytes of the file corresponding to the Transferable. + This attribute can be None as the size of the Transferable is only + provided if the TransferableRange that refers to the object is the last. + + """ + + id: uuid.UUID # noqa: VNE003 + name: str + user_profile_id: uuid.UUID + user_provided_meta: Dict[str, str] + sha1: Optional[bytes] + size: Optional[int] + + +class TransferableRange(pydantic.BaseModel): + """A TransferableRange sent as part of an OnTheWirePacket. + + Attributes: + transferable: the Transferable corresponding to the TransferableRange. + is_last: boolean indicating if it is the last range for this Transferable. + byte_offset: the start position of this range in the related Transferable. + data: the data payload of the TransferableRange i.e. a chunk of the file + of the related Transferable. + + """ + + transferable: Transferable + is_last: bool + byte_offset: int + data: bytes + + +class TransferableRevocation(pydantic.BaseModel): + """A TransferableRevocation sent as part of an OnTheWirePacket. + + Attributes: + transferable_id: a UUID identifying the revoked Transferable. + The identifier is the same on the origin and destination side. + user_profile_id: the UUID of the user profile owning the concerned Transferable + on the origin side. + reason: why the transferable has been revoked. + transferable_name: the filename of the transferable in the revocation. + transferable_sha1: the SHA-1 of the transferable in the revocation. + + """ + + transferable_id: uuid.UUID + user_profile_id: uuid.UUID + reason: enums.TransferableRevocationReason + transferable_name: str + transferable_sha1: Optional[bytes] + + +class HistoryEntry(pydantic.BaseModel): + """A history entry logging a processed Transferable on the origin side. + + Attributes: + transferable_id: a UUID identifying the concerned Transferable. + The identifier is the same on the origin and destination side. + user_profile_id: the UUID of the user profile owning the concerned Transferable + on the origin side. Used to notify the user on the destination side if none + of the concerned TransferableRanges was received. + state: the state of the OutgoingTransferable after processing. + name: the filename of the OutgoingTransferable. + sha1: the SHA-1 of the OutgoingTransferable. + user_provided_meta: metadata provided by the user. + + """ + + transferable_id: uuid.UUID + user_profile_id: uuid.UUID + state: enums.OutgoingTransferableState + name: str + sha1: Optional[bytes] + user_provided_meta: Optional[Dict[str, str]] + + @pydantic.validator("state") + def _check_state_is_final( + cls, state: enums.OutgoingTransferableState # noqa: N805 + ) -> enums.OutgoingTransferableState: + if not state.is_final: + raise ValueError( + f"State must be final i.e. must be one of {state.get_final_states()}" + ) + + return state + + +class History(pydantic.BaseModel): + """A History of processed Transferables sent as part of an OnTheWirePacket. + + Attributes: + entries: HistoryEntries that make up the History. + + """ + + entries: List[HistoryEntry] + + +class SerializationError(RuntimeError): + """Signal an error encountered while serializing an OnTheWirePacket to bytes.""" + + +class DeserializationError(RuntimeError): + """Signal an error encountered while deserializing an OnTheWirePacket from bytes.""" + + +def _pack_default(obj: Any) -> Any: + """Default converts UUID to str when packing with MessagePack.""" + if isinstance(obj, uuid.UUID): + obj = str(obj) + return obj + + +class OnTheWirePacket(pydantic.BaseModel): + """A packet of data and metadata sent over the network. + + Attributes: + transferable_ranges: a list of the TransferableRanges in the packet. + transferable_revocations: a list of the TransferableRevocations in the packet. + history: an optional History of processed Transferables. + + """ + + transferable_ranges: List[TransferableRange] = [] + transferable_revocations: List[TransferableRevocation] = [] + history: Optional[History] + + def to_bytes(self) -> bytes: + """Serialize the OnTheWirePacket object to bytes. + + Returns: + The bytes of the serialized OnTheWirePacket. + + Raises: + SerializationError: if the serialization fails. + + """ + try: + return msgpack.packb(self.dict(), default=_pack_default) + except Exception as exc: + raise SerializationError from exc + + @classmethod + def from_bytes(cls, data: bytes) -> "OnTheWirePacket": + """Deserialize an OnTheWirePacket object from bytes. + + Args: + data: bytes corresponding to a serialized OnTheWirePacket. + + Returns: + The OnTheWirePacket object resulting from the deserialization. + + Raises: + DeserializationError: if the deserialization fails. + + """ + try: + unpacked = msgpack.unpackb(data) + return cls.parse_obj(unpacked) + except Exception as exc: + raise DeserializationError from exc + + def is_empty(self) -> bool: + """Check if packet is empty. + + Returns: + True if the packet has no TransferableRanges, TransferableRevocations + and OngoingHistory. + + """ + return ( + len(self.transferable_ranges) == len(self.transferable_revocations) == 0 + ) and self.history is None + + def __str__(self) -> str: + self.history: Optional[History] = self.history # pytype + return ( + "OnTheWirePacket<" + f"transferable ranges: {len(self.transferable_ranges)}, " + f"revocations: {len(self.transferable_revocations)}, " + f"history entries: {len(self.history.entries) if self.history else 0 }" + ">" + ) + + +__all__ = ( + "Transferable", + "TransferableRange", + "TransferableRevocation", + "HistoryEntry", + "History", + "SerializationError", + "DeserializationError", + "OnTheWirePacket", +) diff --git a/backend/eurydice/common/redoc/__init__.py b/backend/eurydice/common/redoc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/common/redoc/apps.py b/backend/eurydice/common/redoc/apps.py new file mode 100644 index 0000000..06fdd62 --- /dev/null +++ b/backend/eurydice/common/redoc/apps.py @@ -0,0 +1,6 @@ +from django import apps + + +class ReDocConfig(apps.AppConfig): + name = "eurydice.common.redoc" + label = "eurydice_common_redoc" diff --git a/backend/eurydice/common/redoc/static/redoc/redoc.standalone@2.0.0-rc.56.js b/backend/eurydice/common/redoc/static/redoc/redoc.standalone@2.0.0-rc.56.js new file mode 100644 index 0000000..3ec9d33 --- /dev/null +++ b/backend/eurydice/common/redoc/static/redoc/redoc.standalone@2.0.0-rc.56.js @@ -0,0 +1,3 @@ +/*! For license information please see redoc.standalone.js.LICENSE.txt */ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("null")):"function"==typeof define&&define.amd?define(["null"],t):"object"==typeof exports?exports.Redoc=t(require("null")):e.Redoc=t(e.null)}(this,(function(e){return function(){var t={7228:function(e){e.exports=function(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n=e.length?{done:!0}:{done:!1,value:e[o++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,s=!0,l=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return s=e.done,e},e:function(e){l=!0,a=e},f:function(){try{s||null==n.return||n.return()}finally{if(l)throw a}}}},e.exports.default=e.exports,e.exports.__esModule=!0},9842:function(e,t,n){var r=n(9754),o=n(7067),i=n(8585);e.exports=function(e){var t=o();return function(){var n,o=r(e);if(t){var a=r(this).constructor;n=Reflect.construct(o,arguments,a)}else n=o.apply(this,arguments);return i(this,n)}},e.exports.default=e.exports,e.exports.__esModule=!0},9713:function(e){e.exports=function(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e},e.exports.default=e.exports,e.exports.__esModule=!0},9754:function(e){function t(n){return e.exports=t=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)},e.exports.default=e.exports,e.exports.__esModule=!0,t(n)}e.exports=t,e.exports.default=e.exports,e.exports.__esModule=!0},2205:function(e,t,n){var r=n(9489);e.exports=function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&r(e,t)},e.exports.default=e.exports,e.exports.__esModule=!0},430:function(e){e.exports=function(e){return-1!==Function.toString.call(e).indexOf("[native code]")},e.exports.default=e.exports,e.exports.__esModule=!0},7067:function(e){e.exports=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}},e.exports.default=e.exports,e.exports.__esModule=!0},6860:function(e){e.exports=function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)},e.exports.default=e.exports,e.exports.__esModule=!0},3884:function(e){e.exports=function(e,t){var n=e&&("undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"]);if(null!=n){var r,o,i=[],a=!0,s=!1;try{for(n=n.call(e);!(a=(r=n.next()).done)&&(i.push(r.value),!t||i.length!==t);a=!0);}catch(e){s=!0,o=e}finally{try{a||null==n.return||n.return()}finally{if(s)throw o}}return i}},e.exports.default=e.exports,e.exports.__esModule=!0},521:function(e){e.exports=function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")},e.exports.default=e.exports,e.exports.__esModule=!0},8206:function(e){e.exports=function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")},e.exports.default=e.exports,e.exports.__esModule=!0},8585:function(e,t,n){var r=n(8).default,o=n(1506);e.exports=function(e,t){return!t||"object"!==r(t)&&"function"!=typeof t?o(e):t},e.exports.default=e.exports,e.exports.__esModule=!0},9489:function(e){function t(n,r){return e.exports=t=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},e.exports.default=e.exports,e.exports.__esModule=!0,t(n,r)}e.exports=t,e.exports.default=e.exports,e.exports.__esModule=!0},3038:function(e,t,n){var r=n(2858),o=n(3884),i=n(379),a=n(521);e.exports=function(e,t){return r(e)||o(e,t)||i(e,t)||a()},e.exports.default=e.exports,e.exports.__esModule=!0},319:function(e,t,n){var r=n(3646),o=n(6860),i=n(379),a=n(8206);e.exports=function(e){return r(e)||o(e)||i(e)||a()},e.exports.default=e.exports,e.exports.__esModule=!0},8:function(e){function t(n){return"function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?(e.exports=t=function(e){return typeof e},e.exports.default=e.exports,e.exports.__esModule=!0):(e.exports=t=function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e.exports.default=e.exports,e.exports.__esModule=!0),t(n)}e.exports=t,e.exports.default=e.exports,e.exports.__esModule=!0},379:function(e,t,n){var r=n(7228);e.exports=function(e,t){if(e){if("string"==typeof e)return r(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?r(e,t):void 0}},e.exports.default=e.exports,e.exports.__esModule=!0},5957:function(e,t,n){var r=n(9754),o=n(9489),i=n(430),a=n(9100);function s(t){var n="function"==typeof Map?new Map:void 0;return e.exports=s=function(e){if(null===e||!i(e))return e;if("function"!=typeof e)throw new TypeError("Super expression must either be null or a function");if(void 0!==n){if(n.has(e))return n.get(e);n.set(e,t)}function t(){return a(e,arguments,r(this).constructor)}return t.prototype=Object.create(e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),o(t,e)},e.exports.default=e.exports,e.exports.__esModule=!0,s(t)}e.exports=s,e.exports.default=e.exports,e.exports.__esModule=!0},7757:function(e,t,n){e.exports=n(5666)},2840:function(e,t,n){"use strict";var r=n(319).default,o=n(7757);n(1539),n(8674),n(9601),n(2222),n(1249),n(8309),n(7941),n(7327),n(4916),n(3123),n(7042);var i=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))((function(o,i){function a(e){try{l(r.next(e))}catch(e){i(e)}}function s(e){try{l(r.throw(e))}catch(e){i(e)}}function l(e){var t;e.done?o(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(a,s)}l((r=r.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:!0}),t.bundleDocument=t.bundle=t.OasVersion=void 0;var a,s=n(2307),l=n(8604),c=n(9079),u=n(8553),p=n(4343),f=n(7649),d=n(9562),h=n(3353),m=n(6230),v=n(8140),g=n(4241),y=n(2806),b=n(9272);function x(e){return i(this,void 0,void 0,o.mark((function t(){var n,i,s,v,y,b,x,w,E,S,_,O,A,I,C;return o.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return n=e.document,i=e.config,s=e.customTypes,v=e.externalRefResolver,y=e.dereference,b=void 0!==y&&y,x=m.detectOpenAPI(n.parsed),w=m.openAPIMajor(x),E=i.getRulesForOasVersion(w),S=d.normalizeTypes(i.extendTypes((null!=s?s:w===m.OasMajorVersion.Version3)?x===a.Version3_1?f.Oas3_1Types:u.Oas3Types:p.Oas2Types,x),i),_=g.initRules(E,i,"preprocessors",x),O=g.initRules(E,i,"decorators",x),A={problems:[],oasVersion:x},I=c.normalizeVisitors([].concat(r(_),[{severity:"error",ruleId:"bundler",visitor:k(w,b,n)}],r(O)),S),t.next=11,l.resolveDocument({rootDocument:n,rootType:S.DefinitionRoot,externalRefResolver:v});case 11:return C=t.sent,h.walkDocument({document:n,rootType:S.DefinitionRoot,normalizedVisitors:I,resolvedRefMap:C,ctx:A}),t.abrupt("return",{bundle:n,problems:A.problems.map((function(e){return i.addProblemToIgnore(e)})),fileDependencies:v.getFiles()});case 14:case"end":return t.stop()}}),t)})))}function w(e,t){switch(t){case m.OasMajorVersion.Version3:switch(e){case"Schema":return"schemas";case"Parameter":return"parameters";case"Response":return"responses";case"Example":return"examples";case"RequestBody":return"requestBodies";case"Header":return"headers";case"SecuritySchema":return"securitySchemes";case"Link":return"links";case"Callback":return"callbacks";default:return null}case m.OasMajorVersion.Version2:switch(e){case"Schema":return"definitions";case"Parameter":return"parameters";case"Response":return"responses";default:return null}}}function k(e,t,n){var r,o={ref:{leave:function(r,o,s){if(s.location&&void 0!==s.node){if(s.location.source!==n.source||s.location.source!==o.location.source||"scalar"===o.type.name||t){var l=w(o.type.name,e);l?t?(a(l,s,o),i(r,s,o)):r.$ref=a(l,s,o):i(r,s,o)}}else y.reportUnresolvedRef(s,o.report,o.location)}},DefinitionRoot:{enter:function(t){e===m.OasMajorVersion.Version3?r=t.components=t.components||{}:e===m.OasMajorVersion.Version2&&(r=t)}}};function i(e,t,n){b.isPlainObject(t.node)?(delete e.$ref,Object.assign(e,t.node)):n.parent[n.key]=t.node}function a(t,n,o){r[t]=r[t]||{};var i=function(e,t,n){for(var o=[e.location.source.absoluteRef,e.location.pointer],i=o[0],a=o[1],l=r[t],c="",u=a.slice(2).split("/").filter(Boolean);u.length>0;)if(c=u.pop()+(c?"-".concat(c):""),!l||!l[c]||s(l[c],e.node))return c;if(!l[c=v.refBaseName(i)+(c?"_".concat(c):"")]||s(l[c],e.node))return c;for(var p=c,f=2;l[c]&&!s(l[c],e.node);)c="".concat(p,"-").concat(f),f++;return l[c]||n.report({message:"Two schemas are referenced with the same name but different content. Renamed ".concat(p," to ").concat(c,"."),location:n.location,forceSeverity:"warn"}),c}(n,t,o);return r[t][i]=n.node,e===m.OasMajorVersion.Version3?"#/components/".concat(t,"/").concat(i):"#/".concat(t,"/").concat(i)}return e===m.OasMajorVersion.Version3&&(o.DiscriminatorMapping={leave:function(n,r){for(var o=0,i=Object.keys(n);o1&&void 0!==arguments[1]?arguments[1]:"";if(!e)return[];var n=require,r=new Map;return e.map((function(e){var o="string"==typeof e?n(u.resolve(u.dirname(t),e)):e,i=o.id;if("string"!=typeof i)throw new Error(d.red("Plugin must define `id` property in ".concat(d.blue(e.toString()),".")));if(r.has(i)){var a=r.get(i);throw new Error(d.red('Plugin "id" must be unique. Plugin '.concat(d.blue(e.toString()),' uses id "').concat(d.blue(i),'" already seen in ').concat(d.blue(a))))}r.set(i,e.toString());var s=Object.assign(Object.assign({id:i},o.configs?{configs:o.configs}:{}),o.typeExtension?{typeExtension:o.typeExtension}:{});if(o.rules){if(!o.rules.oas3&&!o.rules.oas2)throw new Error('Plugin rules must have `oas3` or `oas2` rules "'.concat(e,"."));s.rules={},o.rules.oas3&&(s.rules.oas3=y(o.rules.oas3,i)),o.rules.oas2&&(s.rules.oas2=y(o.rules.oas2,i))}if(o.preprocessors){if(!o.preprocessors.oas3&&!o.preprocessors.oas2)throw new Error('Plugin `preprocessors` must have `oas3` or `oas2` preprocessors "'.concat(e,"."));s.preprocessors={},o.preprocessors.oas3&&(s.preprocessors.oas3=y(o.preprocessors.oas3,i)),o.preprocessors.oas2&&(s.preprocessors.oas2=y(o.preprocessors.oas2,i))}if(o.decorators){if(!o.decorators.oas3&&!o.decorators.oas2)throw new Error('Plugin `decorators` must have `oas3` or `oas2` decorators "'.concat(e,"."));s.decorators={},o.decorators.oas3&&(s.decorators.oas3=y(o.decorators.oas3,i)),o.decorators.oas2&&(s.decorators.oas2=y(o.decorators.oas2,i))}return s})).filter(h.notUndefined)}(n.plugins,o):[],this.doNotResolveExamples=!!n.doNotResolveExamples,n.extends||(this.recommendedFallback=!0);var w,k,E=n.extends?(w=n.extends,k=this.plugins,w.map((function(e){var t,n=function(e){if(e.indexOf("/")>-1){var t=e.split("/"),n=r(t,2);return{pluginId:n[0],configName:n[1]}}return{pluginId:"",configName:e}}(e),o=n.pluginId,i=n.configName,a=k.find((function(e){return e.id===o}));if(!a)throw new Error("Invalid config ".concat(d.red(e),": plugin ").concat(o," is not included."));var s=null===(t=a.configs)||void 0===t?void 0:t[i];if(!s)throw new Error(o?"Invalid config ".concat(d.red(e),": plugin ").concat(o," doesn't export config with name ").concat(i,"."):"Invalid config ".concat(d.red(e),": there is no such built-in config."));return s}))):[v.default];(n.rules||n.preprocessors||n.decorators)&&E.push({rules:n.rules,preprocessors:n.preprocessors,decorators:n.decorators});var S=function(e){var t,n={rules:{},oas2Rules:{},oas3_0Rules:{},oas3_1Rules:{},preprocessors:{},oas2Preprocessors:{},oas3_0Preprocessors:{},oas3_1Preprocessors:{},decorators:{},oas2Decorators:{},oas3_0Decorators:{},oas3_1Decorators:{}},r=i(e);try{for(r.s();!(t=r.n()).done;){var o=t.value;if(o.extends)throw new Error("`extends` is not supported in shared configs yet: ".concat(JSON.stringify(o,null,2),"."));Object.assign(n.rules,o.rules),Object.assign(n.oas2Rules,o.oas2Rules),b(n.oas2Rules,o.rules||{}),Object.assign(n.oas3_0Rules,o.oas3_0Rules),b(n.oas3_0Rules,o.rules||{}),Object.assign(n.oas3_1Rules,o.oas3_1Rules),b(n.oas3_1Rules,o.rules||{}),Object.assign(n.preprocessors,o.preprocessors),Object.assign(n.oas2Preprocessors,o.oas2Preprocessors),b(n.oas2Preprocessors,o.preprocessors||{}),Object.assign(n.oas3_0Preprocessors,o.oas3_0Preprocessors),b(n.oas3_0Preprocessors,o.preprocessors||{}),Object.assign(n.oas3_1Preprocessors,o.oas3_1Preprocessors),b(n.oas3_1Preprocessors,o.preprocessors||{}),Object.assign(n.decorators,o.decorators),Object.assign(n.oas2Decorators,o.oas2Decorators),b(n.oas2Decorators,o.decorators||{}),Object.assign(n.oas3_0Decorators,o.oas3_0Decorators),b(n.oas3_0Decorators,o.decorators||{}),Object.assign(n.oas3_1Decorators,o.oas3_1Decorators),b(n.oas3_1Decorators,o.decorators||{})}}catch(e){r.e(e)}finally{r.f()}return n}(E);this.rules=(a(l={},m.OasVersion.Version2,Object.assign(Object.assign({},S.rules),S.oas2Rules)),a(l,m.OasVersion.Version3_0,Object.assign(Object.assign({},S.rules),S.oas3_0Rules)),a(l,m.OasVersion.Version3_1,Object.assign(Object.assign({},S.rules),S.oas3_1Rules)),l),this.preprocessors=(a(g={},m.OasVersion.Version2,Object.assign(Object.assign({},S.preprocessors),S.oas2Preprocessors)),a(g,m.OasVersion.Version3_0,Object.assign(Object.assign({},S.preprocessors),S.oas3_0Preprocessors)),a(g,m.OasVersion.Version3_1,Object.assign(Object.assign({},S.preprocessors),S.oas3_1Preprocessors)),g),this.decorators=(a(x={},m.OasVersion.Version2,Object.assign(Object.assign({},S.decorators),S.oas2Decorators)),a(x,m.OasVersion.Version3_0,Object.assign(Object.assign({},S.decorators),S.oas3_0Decorators)),a(x,m.OasVersion.Version3_1,Object.assign(Object.assign({},S.decorators),S.oas3_1Decorators)),x);var _=this.configFile?u.dirname(this.configFile):"undefined"!=typeof process&&process.cwd()||"",O=u.join(_,t.bD);if(c.hasOwnProperty("existsSync")&&c.existsSync(O)){this.ignore=p.safeLoad(c.readFileSync(O,"utf-8"))||{};for(var A=0,I=Object.keys(this.ignore);A-1}},8604:function(e,t,n){"use strict";var r=n(3269).default,o=n(7757),i=n(3038).default,a=n(1506).default,s=n(2205).default,l=n(9842).default,c=n(5957).default,u=n(4575).default,p=n(3913).default;n(1539),n(8674),n(4916),n(3123),n(4723),n(6992),n(1532),n(8783),n(3948),n(189),n(1038),n(6699),n(2222),n(8309),n(7941),n(9601);var f=this&&this.__awaiter||function(e,t,n,r){return new(n||(n=Promise))((function(o,i){function a(e){try{l(r.next(e))}catch(e){i(e)}}function s(e){try{l(r.throw(e))}catch(e){i(e)}}function l(e){var t;e.done?o(e.value):(t=e.value,t instanceof n?t:new n((function(e){e(t)}))).then(a,s)}l((r=r.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:!0}),t.resolveDocument=t.BaseResolver=t.makeDocumentFromString=t.YamlParseError=t.ResolveError=t.Source=void 0;var d=n(3817),h=n(6470),m=n(8575),v=n(4756),g=n(8140),y=n(9562),b=n(9272),x=function(){function e(t,n,r){u(this,e),this.absoluteRef=t,this.body=n,this.mimeType=r}return p(e,[{key:"getAst",value:function(e){var t;return void 0===this._ast&&(this._ast=null!==(t=e(this.body,{filename:this.absoluteRef}))&&void 0!==t?t:void 0,this._ast&&0===this._ast.kind&&""===this._ast.value&&1!==this._ast.startPosition&&(this._ast.startPosition=1,this._ast.endPosition=1)),this._ast}},{key:"getLines",value:function(){return void 0===this._lines&&(this._lines=this.body.split(/\r\n|[\n\r]/g)),this._lines}}]),e}();t.Source=x;var w=function(e){s(n,e);var t=l(n);function n(e){var r;return u(this,n),(r=t.call(this,e.message)).originalError=e,Object.setPrototypeOf(a(r),n.prototype),r}return n}(c(Error));t.ResolveError=w;var k=/at line (\d+), column (\d+):/,E=function(e){s(n,e);var t=l(n);function n(e,r){var o;u(this,n),(o=t.call(this,e.message.split("\n")[0])).originalError=e,o.source=r,Object.setPrototypeOf(a(o),n.prototype);var s=o.message.match(k)||[],l=i(s,3),c=l[1],p=l[2];return o.line=parseInt(c,10),o.col=parseInt(p,10),o}return n}(c(Error));t.YamlParseError=E,t.makeDocumentFromString=function(e,t){var n=new x(t,e);try{return{source:n,parsed:v.safeLoad(e,{filename:t})}}catch(e){throw new E(e,n)}};var S=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{http:{headers:[]}};u(this,e),this.config=t,this.cache=new Map}return p(e,[{key:"getFiles",value:function(){return new Set(Array.from(this.cache.keys()))}},{key:"resolveExternalRef",value:function(e,t){return g.isAbsoluteUrl(t)?t:e&&g.isAbsoluteUrl(e)?m.resolve(e,t):h.resolve(e?h.dirname(e):process.cwd(),t)}},{key:"loadExternalRef",value:function(e){return f(this,void 0,void 0,o.mark((function t(){var n,r,i;return o.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:if(t.prev=0,!g.isAbsoluteUrl(e)){t.next=10;break}return t.next=4,b.readFileFromUrl(e,this.config.http);case 4:return n=t.sent,r=n.body,i=n.mimeType,t.abrupt("return",new x(e,r,i));case 10:return t.t0=x,t.t1=e,t.next=14,d.promises.readFile(e,"utf-8");case 14:return t.t2=t.sent,t.abrupt("return",new t.t0(t.t1,t.t2));case 16:t.next=21;break;case 18:throw t.prev=18,t.t3=t.catch(0),new w(t.t3);case 21:case"end":return t.stop()}}),t,this,[[0,18]])})))}},{key:"parseDocument",value:function(e){var t,n=arguments.length>1&&void 0!==arguments[1]&&arguments[1],r=e.absoluteRef.substr(e.absoluteRef.lastIndexOf("."));if(![".json",".json",".yml",".yaml"].includes(r)&&!(null===(t=e.mimeType)||void 0===t?void 0:t.match(/(json|yaml|openapi)/))&&!n)return{source:e,parsed:e.body};try{return{source:e,parsed:v.safeLoad(e.body,{filename:e.absoluteRef})}}catch(t){throw new E(t,e)}}},{key:"resolveDocument",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];return f(this,void 0,void 0,o.mark((function r(){var i,a,s,l=this;return o.wrap((function(r){for(;;)switch(r.prev=r.next){case 0:if(i=this.resolveExternalRef(e,t),!(a=this.cache.get(i))){r.next=4;break}return r.abrupt("return",a);case 4:return s=this.loadExternalRef(i).then((function(e){return l.parseDocument(e,n)})),this.cache.set(i,s),r.abrupt("return",s);case 7:case"end":return r.stop()}}),r,this)})))}}]),e}();function _(e,t){return{prev:e,node:t}}function O(e,t){for(;e;){if(e.node===t)return!0;e=e.prev}return!1}t.BaseResolver=S;var A={name:"unknown",properties:{}},I={name:"scalar",properties:{}};t.resolveDocument=function(e){return f(this,void 0,void 0,o.mark((function t(){var n,i,a,s,l,c,u,p;return o.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:p=function(e,t,n,a){function u(e,t,n){return f(this,void 0,void 0,o.mark((function a(){var l,c,p,f,d,h,m,v,y,b,x,w,k;return o.wrap((function(o){for(;;)switch(o.prev=o.next){case 0:if(!O(n.prev,t)){o.next=2;break}throw new Error("Self-referencing circular pointer");case 2:if(l=g.parseRef(t.$ref),c=l.uri,p=l.pointer,f=null!==c,o.prev=4,!f){o.next=11;break}return o.next=8,i.resolveDocument(e.source.absoluteRef,c);case 8:o.t0=o.sent,o.next=12;break;case 11:o.t0=e;case 12:d=o.t0,o.next=21;break;case 15:return o.prev=15,o.t1=o.catch(4),h={resolved:!1,isRemote:f,document:void 0,error:o.t1},m=e.source.absoluteRef+"::"+t.$ref,s.set(m,h),o.abrupt("return",h);case 21:v={resolved:!0,document:d,isRemote:f,node:e.parsed,nodePointer:"#/"},y=d.parsed,b=r(p),o.prev=25,b.s();case 27:if((x=b.n()).done){o.next=55;break}if(w=x.value,"object"==typeof y){o.next=34;break}return y=void 0,o.abrupt("break",55);case 34:if(void 0===y[w]){o.next=39;break}y=y[w],v.nodePointer=g.joinPointer(v.nodePointer,g.escapePointer(w)),o.next=53;break;case 39:if(!g.isRef(y)){o.next=51;break}return o.next=42,u(d,y,_(n,y));case 42:if(v=o.sent,d=v.document||d,"object"==typeof v.node){o.next=47;break}return y=void 0,o.abrupt("break",55);case 47:y=v.node[w],v.nodePointer=g.joinPointer(v.nodePointer,g.escapePointer(w)),o.next=53;break;case 51:return y=void 0,o.abrupt("break",55);case 53:o.next=27;break;case 55:o.next=60;break;case 57:o.prev=57,o.t2=o.catch(25),b.e(o.t2);case 60:return o.prev=60,b.f(),o.finish(60);case 63:if(v.node=y,v.document=d,k=e.source.absoluteRef+"::"+t.$ref,!v.document||!g.isRef(y)){o.next=70;break}return o.next=69,u(v.document,y,_(n,y));case 69:v=o.sent;case 70:return s.set(k,v),o.abrupt("return",Object.assign({},v));case 72:case"end":return o.stop()}}),a,null,[[4,15],[25,57,60,63]])})))}!function e(n,r,o){if("object"==typeof n&&null!==n){var i="".concat(r.name,"::").concat(o);if(!l.has(i))if(l.add(i),Array.isArray(n)){var a=r.items;if(r!==A&&void 0===a)return;for(var s=0;s1&&void 0!==arguments[1]?arguments[1]:{},n={},o=0,i=Object.keys(e);o4&&void 0!==arguments[4]?arguments[4]:[];if(!s.includes(t)){s=[].concat(r(s),[t]);for(var l=new Set,c=0,u=Object.values(t.properties);c3&&void 0!==arguments[3]?arguments[3]:0,a=Object.keys(t);if(0===i)a.push("any"),a.push("ref");else{if(r.any)throw new Error("any() is allowed only on top level");if(r.ref)throw new Error("ref() is allowed only on top level")}for(var s=0,l=a;s1&&void 0!==arguments[1]?arguments[1]:C.source.absoluteRef;if(!i.isRef(e))return{location:m,node:e};var n=t+"::"+e.$ref,r=p.get(n);if(!r)return{location:void 0,node:void 0};var o=r.resolved,s=r.node,l=r.document,c=r.nodePointer,u=r.error,f=o?new i.Location(l.source,c):u instanceof a.YamlParseError?new i.Location(u.source,""):void 0;return{location:f,node:s,error:u}}function Ne(e,t,n){var r=n.location?Array.isArray(n.location)?n.location:[n.location]:[Object.assign(Object.assign({},C),{reportOnKey:!1})];f.problems.push(Object.assign(Object.assign({ruleId:e,severity:n.forceSeverity||t},n),{suggest:n.suggest||[],location:r.map((function(e){return Object.assign(Object.assign(Object.assign({},C),{reportOnKey:!1}),e)}))}))}}(t.parsed,n,new i.Location(t.source,"#/"),void 0,"")}},4756:function(e,t,n){"use strict";var r=n(9439);e.exports=r},9439:function(e,t,n){"use strict";var r=n(5143),o=n(9084);function i(e){return function(){throw new Error("Function "+e+" is deprecated and cannot be used.")}}e.exports.Type=n(889),e.exports.Schema=n(304),e.exports.FAILSAFE_SCHEMA=n(4801),e.exports.JSON_SCHEMA=n(2437),e.exports.CORE_SCHEMA=n(9533),e.exports.DEFAULT_SAFE_SCHEMA=n(7265),e.exports.DEFAULT_FULL_SCHEMA=n(1493),e.exports.load=r.load,e.exports.loadAll=r.loadAll,e.exports.safeLoad=r.safeLoad,e.exports.safeLoadAll=r.safeLoadAll,e.exports.dump=o.dump,e.exports.safeDump=o.safeDump,e.exports.YAMLException=n(2188),e.exports.MINIMAL_SCHEMA=n(4801),e.exports.SAFE_SCHEMA=n(7265),e.exports.DEFAULT_SCHEMA=n(1493),e.exports.scan=i("scan"),e.exports.parse=i("parse"),e.exports.compose=i("compose"),e.exports.addConstructor=i("addConstructor")},910:function(e,t,n){"use strict";function r(e){return null==e}n(7941),n(9653),n(2481),e.exports.isNothing=r,e.exports.isObject=function(e){return"object"==typeof e&&null!==e},e.exports.toArray=function(e){return Array.isArray(e)?e:r(e)?[]:[e]},e.exports.repeat=function(e,t){var n,r="";for(n=0;n>10),56320+(e-65536&1023))}for(var k=new Array(256),E=new Array(256),S=0;S<256;S++)k[S]=x(S)?1:0,E[S]=x(S);function _(e,t){this.input=e,this.filename=t.filename||null,this.schema=t.schema||s,this.onWarning=t.onWarning||null,this.legacy=t.legacy||!1,this.json=t.json||!1,this.listener=t.listener||null,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=e.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.documents=[]}function O(e,t){return new o(t,new i(e.filename,e.input,e.position,e.line,e.position-e.lineStart))}function A(e,t){throw O(e,t)}function I(e,t){e.onWarning&&e.onWarning.call(null,O(e,t))}var C={YAML:function(e,t,n){var r,o,i;null!==e.version&&A(e,"duplication of %YAML directive"),1!==n.length&&A(e,"YAML directive accepts exactly one argument"),null===(r=/^([0-9]+)\.([0-9]+)$/.exec(n[0]))&&A(e,"ill-formed argument of the YAML directive"),o=parseInt(r[1],10),i=parseInt(r[2],10),1!==o&&A(e,"unacceptable YAML version of the document"),e.version=n[0],e.checkLineBreaks=i<2,1!==i&&2!==i&&I(e,"unsupported YAML version of the document")},TAG:function(e,t,n){var r,o;2!==n.length&&A(e,"TAG directive accepts exactly two arguments"),r=n[0],o=n[1],f.test(r)||A(e,"ill-formed tag handle (first argument) of the TAG directive"),l.call(e.tagMap,r)&&A(e,'there is a previously declared suffix for "'+r+'" tag handle'),d.test(o)||A(e,"ill-formed tag prefix (second argument) of the TAG directive"),e.tagMap[r]=o}};function T(e,t,n,r){var o,i,a,s;if(t1&&(e.result+=r.repeat("\n",t-1))}function D(e,t){var n,r,o=e.tag,i=e.anchor,a=[],s=!1;for(null!==e.anchor&&(e.anchorMap[e.anchor]=a),r=e.input.charCodeAt(e.position);0!==r&&45===r&&g(e.input.charCodeAt(e.position+1));)if(s=!0,e.position++,L(e,!0,-1)&&e.lineIndent<=t)a.push(null),r=e.input.charCodeAt(e.position);else if(n=e.line,U(e,t,3,!1,!0),a.push(e.result),L(e,!0,-1),r=e.input.charCodeAt(e.position),(e.line===n||e.lineIndent>t)&&0!==r)A(e,"bad indentation of a sequence entry");else if(e.lineIndentt?x=1:e.lineIndent===t?x=0:e.lineIndentt?x=1:e.lineIndent===t?x=0:e.lineIndentt)&&(U(e,t,4,!0,o)&&(m?d=e.result:h=e.result),m||(P(e,u,p,f,d,h,i,a),f=d=h=null),L(e,!0,-1),s=e.input.charCodeAt(e.position)),e.lineIndent>t&&0!==s)A(e,"bad indentation of a mapping entry");else if(e.lineIndent=0))break;0===i?A(e,"bad explicit indentation width of a block scalar; it cannot be less than one"):u?A(e,"repeat of an indentation width identifier"):(p=t+i-1,u=!0)}if(v(a)){do{a=e.input.charCodeAt(++e.position)}while(v(a));if(35===a)do{a=e.input.charCodeAt(++e.position)}while(!m(a)&&0!==a)}for(;0!==a;){for(j(e),e.lineIndent=0,a=e.input.charCodeAt(e.position);(!u||e.lineIndentp&&(p=e.lineIndent),m(a))f++;else{if(e.lineIndent0){for(o=a,i=0;o>0;o--)(a=b(s=e.input.charCodeAt(++e.position)))>=0?i=(i<<4)+a:A(e,"expected hexadecimal character");e.result+=w(i),e.position++}else A(e,"unknown escape sequence");n=r=e.position}else m(s)?(T(e,n,r,!0),M(e,L(e,!1,t)),n=r=e.position):e.position===e.lineStart&&N(e)?A(e,"unexpected end of the document within a double quoted scalar"):(e.position++,r=e.position)}A(e,"unexpected end of the stream within a double quoted scalar")}(e,d)?_=!0:function(e){var t,n,r;if(42!==(r=e.input.charCodeAt(e.position)))return!1;for(r=e.input.charCodeAt(++e.position),t=e.position;0!==r&&!g(r)&&!y(r);)r=e.input.charCodeAt(++e.position);return e.position===t&&A(e,"name of an alias node must contain at least one character"),n=e.input.slice(t,e.position),l.call(e.anchorMap,n)||A(e,'unidentified alias "'+n+'"'),e.result=e.anchorMap[n],L(e,!0,-1),!0}(e)?(_=!0,null===e.tag&&null===e.anchor||A(e,"alias node should not have any properties")):function(e,t,n){var r,o,i,a,s,l,c,u,p=e.kind,f=e.result;if(g(u=e.input.charCodeAt(e.position))||y(u)||35===u||38===u||42===u||33===u||124===u||62===u||39===u||34===u||37===u||64===u||96===u)return!1;if((63===u||45===u)&&(g(r=e.input.charCodeAt(e.position+1))||n&&y(r)))return!1;for(e.kind="scalar",e.result="",o=i=e.position,a=!1;0!==u;){if(58===u){if(g(r=e.input.charCodeAt(e.position+1))||n&&y(r))break}else if(35===u){if(g(e.input.charCodeAt(e.position-1)))break}else{if(e.position===e.lineStart&&N(e)||n&&y(u))break;if(m(u)){if(s=e.line,l=e.lineStart,c=e.lineIndent,L(e,!1,-1),e.lineIndent>=t){a=!0,u=e.input.charCodeAt(e.position);continue}e.position=i,e.line=s,e.lineStart=l,e.lineIndent=c;break}}a&&(T(e,o,i,!1),M(e,e.line-s),o=i=e.position,a=!1),v(u)||(i=e.position+1),u=e.input.charCodeAt(++e.position)}return T(e,o,i,!1),!!e.result||(e.kind=p,e.result=f,!1)}(e,d,1===n)&&(_=!0,null===e.tag&&(e.tag="?")),null!==e.anchor&&(e.anchorMap[e.anchor]=e.result)):0===x&&(_=c&&D(e,h))),null!==e.tag&&"!"!==e.tag)if("?"===e.tag){for(null!==e.result&&"scalar"!==e.kind&&A(e,'unacceptable node kind for ! tag; it should be "scalar", not "'+e.kind+'"'),u=0,p=e.implicitTypes.length;u tag; it should be "'+f.kind+'", not "'+e.kind+'"'),f.resolve(e.result)?(e.result=f.construct(e.result),null!==e.anchor&&(e.anchorMap[e.anchor]=e.result)):A(e,"cannot resolve a node with !<"+e.tag+"> explicit tag")):A(e,"unknown tag !<"+e.tag+">");return null!==e.listener&&e.listener("close",e),null!==e.tag||null!==e.anchor||_}function B(e){var t,n,r,o,i=e.position,a=!1;for(e.version=null,e.checkLineBreaks=e.legacy,e.tagMap={},e.anchorMap={};0!==(o=e.input.charCodeAt(e.position))&&(L(e,!0,-1),o=e.input.charCodeAt(e.position),!(e.lineIndent>0||37!==o));){for(a=!0,o=e.input.charCodeAt(++e.position),t=e.position;0!==o&&!g(o);)o=e.input.charCodeAt(++e.position);for(r=[],(n=e.input.slice(t,e.position)).length<1&&A(e,"directive name must not be less than one character in length");0!==o;){for(;v(o);)o=e.input.charCodeAt(++e.position);if(35===o){do{o=e.input.charCodeAt(++e.position)}while(0!==o&&!m(o));break}if(m(o))break;for(t=e.position;0!==o&&!g(o);)o=e.input.charCodeAt(++e.position);r.push(e.input.slice(t,e.position))}0!==o&&j(e),l.call(C,n)?C[n](e,n,r):I(e,'unknown document directive "'+n+'"')}L(e,!0,-1),0===e.lineIndent&&45===e.input.charCodeAt(e.position)&&45===e.input.charCodeAt(e.position+1)&&45===e.input.charCodeAt(e.position+2)?(e.position+=3,L(e,!0,-1)):a&&A(e,"directives end mark is expected"),U(e,e.lineIndent-1,4,!1,!0),L(e,!0,-1),e.checkLineBreaks&&u.test(e.input.slice(i,e.position))&&I(e,"non-ASCII line breaks are interpreted as content"),e.documents.push(e.result),e.position===e.lineStart&&N(e)?46===e.input.charCodeAt(e.position)&&(e.position+=3,L(e,!0,-1)):e.position0&&-1==="\0\r\n…\u2028\u2029".indexOf(this.buffer.charAt(o-1));)if(o-=1,this.position-o>t/2-1){n=" ... ",o+=5;break}for(i="",a=this.position;at/2-1){i=" ... ",a-=5;break}return s=this.buffer.slice(o,a),r.repeat(" ",e)+n+s+i+"\n"+r.repeat(" ",e+this.position-o+n.length)+"^"},o.prototype.toString=function(e){var t,n="";return this.name&&(n+='in "'+this.name+'" '),n+="at line "+(this.line+1)+", column "+(this.column+1),e||(t=this.getSnippet())&&(n+=":\n"+t),n},e.exports=o},304:function(e,t,n){"use strict";n(4747),n(7327);var r=n(910),o=n(2188),i=n(889);function a(e,t,n){var r=[];return e.include.forEach((function(e){n=a(e,t,n)})),e[t].forEach((function(e){n.forEach((function(t,n){t.tag===e.tag&&t.kind===e.kind&&r.push(n)})),n.push(e)})),n.filter((function(e,t){return-1===r.indexOf(t)}))}function s(e){this.include=e.include||[],this.implicit=e.implicit||[],this.explicit=e.explicit||[],this.implicit.forEach((function(e){if(e.loadKind&&"scalar"!==e.loadKind)throw new o("There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.")})),this.compiledImplicit=a(this,"implicit",[]),this.compiledExplicit=a(this,"explicit",[]),this.compiledTypeMap=function(){var e,t,n={scalar:{},sequence:{},mapping:{},fallback:{}};function r(e){n[e.kind][e.tag]=n.fallback[e.tag]=e}for(e=0,t=arguments.length;e64)){if(t<0)return!1;r+=6}return r%8==0},construct:function(e){var t,n,o=e.replace(/[\r\n=]/g,""),a=o.length,s=i,l=0,c=[];for(t=0;t>16&255),c.push(l>>8&255),c.push(255&l)),l=l<<6|s.indexOf(o.charAt(t));return 0==(n=a%4*6)?(c.push(l>>16&255),c.push(l>>8&255),c.push(255&l)):18===n?(c.push(l>>10&255),c.push(l>>2&255)):12===n&&c.push(l>>4&255),r?r.from?r.from(c):new r(c):c},predicate:function(e){return r&&r.isBuffer(e)},represent:function(e){var t,n,r="",o=0,a=e.length,s=i;for(t=0;t>18&63],r+=s[o>>12&63],r+=s[o>>6&63],r+=s[63&o]),o=(o<<8)+e[t];return 0==(n=a%3)?(r+=s[o>>18&63],r+=s[o>>12&63],r+=s[o>>6&63],r+=s[63&o]):2===n?(r+=s[o>>10&63],r+=s[o>>4&63],r+=s[o<<2&63],r+=s[64]):1===n&&(r+=s[o>>2&63],r+=s[o<<4&63],r+=s[64],r+=s[64]),r}})},9753:function(e,t,n){"use strict";n(1539);var r=n(889);e.exports=new r("tag:yaml.org,2002:bool",{kind:"scalar",resolve:function(e){if(null===e)return!1;var t=e.length;return 4===t&&("true"===e||"True"===e||"TRUE"===e)||5===t&&("false"===e||"False"===e||"FALSE"===e)},construct:function(e){return"true"===e||"True"===e||"TRUE"===e},predicate:function(e){return"[object Boolean]"===Object.prototype.toString.call(e)},represent:{lowercase:function(e){return e?"true":"false"},uppercase:function(e){return e?"TRUE":"FALSE"},camelcase:function(e){return e?"True":"False"}},defaultStyle:"lowercase"})},293:function(e,t,n){"use strict";n(4603),n(4916),n(9714),n(5306),n(7042),n(9653),n(4747),n(3123),n(1539);var r=n(910),o=n(889),i=new RegExp("^(?:[-+]?(?:0|[1-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$"),a=/^[-+]?[0-9]+e/;e.exports=new o("tag:yaml.org,2002:float",{kind:"scalar",resolve:function(e){return null!==e&&!(!i.test(e)||"_"===e[e.length-1])},construct:function(e){var t,n,r,o;return n="-"===(t=e.replace(/_/g,"").toLowerCase())[0]?-1:1,o=[],"+-".indexOf(t[0])>=0&&(t=t.slice(1)),".inf"===t?1===n?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:".nan"===t?NaN:t.indexOf(":")>=0?(t.split(":").forEach((function(e){o.unshift(parseFloat(e,10))})),t=0,r=1,o.forEach((function(e){t+=e*r,r*=60})),n*t):n*parseFloat(t,10)},predicate:function(e){return"[object Number]"===Object.prototype.toString.call(e)&&(e%1!=0||r.isNegativeZero(e))},represent:function(e,t){var n;if(isNaN(e))switch(t){case"lowercase":return".nan";case"uppercase":return".NAN";case"camelcase":return".NaN"}else if(Number.POSITIVE_INFINITY===e)switch(t){case"lowercase":return".inf";case"uppercase":return".INF";case"camelcase":return".Inf"}else if(Number.NEGATIVE_INFINITY===e)switch(t){case"lowercase":return"-.inf";case"uppercase":return"-.INF";case"camelcase":return"-.Inf"}else if(r.isNegativeZero(e))return"-0.0";return n=e.toString(10),a.test(n)?n.replace("e",".e"):n},defaultStyle:"lowercase"})},5733:function(e,t,n){"use strict";n(7042),n(4916),n(5306),n(4747),n(3123),n(1539),n(9714);var r=n(910),o=n(889);function i(e){return 48<=e&&e<=55}function a(e){return 48<=e&&e<=57}e.exports=new o("tag:yaml.org,2002:int",{kind:"scalar",resolve:function(e){if(null===e)return!1;var t,n,r=e.length,o=0,s=!1;if(!r)return!1;if("-"!==(t=e[o])&&"+"!==t||(t=e[++o]),"0"===t){if(o+1===r)return!0;if("b"===(t=e[++o])){for(o++;o=0?"0b"+e.toString(2):"-0b"+e.toString(2).slice(1)},octal:function(e){return e>=0?"0"+e.toString(8):"-0"+e.toString(8).slice(1)},decimal:function(e){return e.toString(10)},hexadecimal:function(e){return e>=0?"0x"+e.toString(16).toUpperCase():"-0x"+e.toString(16).toUpperCase().slice(1)}},defaultStyle:"decimal",styleAliases:{binary:[2,"bin"],octal:[8,"oct"],decimal:[10,"dec"],hexadecimal:[16,"hex"]}})},9798:function(e,t,n){"use strict";var r;n(4747),n(8309),n(7042),n(1539),n(9714);try{r=n(7707)}catch(e){"undefined"!=typeof window&&(r=window.esprima)}var o=n(889);e.exports=new o("tag:yaml.org,2002:js/function",{kind:"scalar",resolve:function(e){if(null===e)return!1;try{var t="("+e+")",n=r.parse(t,{range:!0});return"Program"===n.type&&1===n.body.length&&"ExpressionStatement"===n.body[0].type&&("ArrowFunctionExpression"===n.body[0].expression.type||"FunctionExpression"===n.body[0].expression.type)}catch(e){return!1}},construct:function(e){var t,n="("+e+")",o=r.parse(n,{range:!0}),i=[];if("Program"!==o.type||1!==o.body.length||"ExpressionStatement"!==o.body[0].type||"ArrowFunctionExpression"!==o.body[0].expression.type&&"FunctionExpression"!==o.body[0].expression.type)throw new Error("Failed to resolve function");return o.body[0].expression.params.forEach((function(e){i.push(e.name)})),t=o.body[0].expression.body.range,"BlockStatement"===o.body[0].expression.body.type?new Function(i,n.slice(t[0]+1,t[1]-1)):new Function(i,"return "+n.slice(t[0],t[1]))},predicate:function(e){return"[object Function]"===Object.prototype.toString.call(e)},represent:function(e){return e.toString()}})},6446:function(e,t,n){"use strict";n(4916),n(7042),n(4603),n(9714),n(1539);var r=n(889);e.exports=new r("tag:yaml.org,2002:js/regexp",{kind:"scalar",resolve:function(e){if(null===e)return!1;if(0===e.length)return!1;var t=e,n=/\/([gim]*)$/.exec(e),r="";if("/"===t[0]){if(n&&(r=n[1]),r.length>3)return!1;if("/"!==t[t.length-r.length-1])return!1}return!0},construct:function(e){var t=e,n=/\/([gim]*)$/.exec(e),r="";return"/"===t[0]&&(n&&(r=n[1]),t=t.slice(1,t.length-r.length-1)),new RegExp(t,r)},predicate:function(e){return"[object RegExp]"===Object.prototype.toString.call(e)},represent:function(e){var t="/"+e.source+"/";return e.global&&(t+="g"),e.multiline&&(t+="m"),e.ignoreCase&&(t+="i"),t}})},1467:function(e,t,n){"use strict";var r=n(889);e.exports=new r("tag:yaml.org,2002:js/undefined",{kind:"scalar",resolve:function(){return!0},construct:function(){},predicate:function(e){return void 0===e},represent:function(){return""}})},127:function(e,t,n){"use strict";var r=n(889);e.exports=new r("tag:yaml.org,2002:map",{kind:"mapping",construct:function(e){return null!==e?e:{}}})},321:function(e,t,n){"use strict";var r=n(889);e.exports=new r("tag:yaml.org,2002:merge",{kind:"scalar",resolve:function(e){return"<<"===e||null===e}})},1608:function(e,t,n){"use strict";var r=n(889);e.exports=new r("tag:yaml.org,2002:null",{kind:"scalar",resolve:function(e){if(null===e)return!0;var t=e.length;return 1===t&&"~"===e||4===t&&("null"===e||"Null"===e||"NULL"===e)},construct:function(){return null},predicate:function(e){return null===e},represent:{canonical:function(){return"~"},lowercase:function(){return"null"},uppercase:function(){return"NULL"},camelcase:function(){return"Null"}},defaultStyle:"lowercase"})},8372:function(e,t,n){"use strict";n(1539);var r=n(889),o=Object.prototype.hasOwnProperty,i=Object.prototype.toString;e.exports=new r("tag:yaml.org,2002:omap",{kind:"sequence",resolve:function(e){if(null===e)return!0;var t,n,r,a,s,l=[],c=e;for(t=0,n=c.length;t1&&console.warn("Replacing with",p),d++}}else{var v=f(u(t,e[n]));if(i.verbose>1&&console.warn((!1===v?m.colour.red:m.colour.green)+"Fragment resolution",e[n],m.colour.normal),!1===v){if(r.parent[r.pkey]={},i.fatal){var g=new Error("Fragment $ref resolution failed "+e[n]);if(!i.promise)throw g;i.promise.reject(g)}}else d++,r.parent[r.pkey]=v,c[e[n]]=r.path.replace("/%24ref","")}else if(l.protocol){var y=s.resolve(o,e[n]).toString();i.verbose>1&&console.warn(m.colour.yellow+"Rewriting external url ref",e[n],"as",y,m.colour.normal),e["x-miro"]=e[n],i.externalRefs[e[n]]&&(i.externalRefs[y]||(i.externalRefs[y]=i.externalRefs[e[n]]),i.externalRefs[y].failed=i.externalRefs[e[n]].failed),e[n]=y}else if(!e["x-miro"]){var b=s.resolve(o,e[n]).toString(),x=!1;i.externalRefs[e[n]]&&(x=i.externalRefs[e[n]].failed),x||(i.verbose>1&&console.warn(m.colour.yellow+"Rewriting external ref",e[n],"as",b,m.colour.normal),e["x-miro"]=e[n],e[n]=b)}}));return p(e,{},(function(e,t,n){h(e,t)&&void 0!==e.$fixed&&delete e.$fixed})),i.verbose>1&&console.warn("Finished fragment resolution"),e}function g(e,t){if(!t.filters||!t.filters.length)return e;var n,o=r(t.filters);try{for(o.s();!(n=o.n()).done;)e=(0,n.value)(e,t)}catch(e){o.e(e)}finally{o.f()}return e}function y(e){return new Promise((function(t,n){(function(e){return new Promise((function(t,n){function r(t,n,r){if(t[n]&&h(t[n],"$ref")){var i=t[n].$ref;if(!i.startsWith("#")){var a="";if(!o[i]){var s=Object.keys(o).find((function(e,t,n){return i.startsWith(e+"/")}));s&&(e.verbose&&console.warn("Found potential subschema at",s),a=(a="/"+(i.split("#")[1]||"").replace(s.split("#")[1]||"")).split("/undefined").join(""),i=s)}if(o[i]||(o[i]={resolved:!1,paths:[],extras:{},description:t[n].description}),o[i].resolved)if(o[i].failed);else if(e.rewriteRefs){var l=o[i].resolvedAt;e.verbose>1&&console.warn("Rewriting ref",i,l),t[n]["x-miro"]=i,t[n].$ref=l+a}else t[n]=f(o[i].data);else o[i].paths.push(r.path),o[i].extras[r.path]=a}}}var o=e.externalRefs;if(e.resolver.depth>0&&e.source===e.resolver.base)return t(o);p(e.openapi.definitions,{identityDetection:!0,path:"#/definitions"},r),p(e.openapi.components,{identityDetection:!0,path:"#/components"},r),p(e.openapi,{identityDetection:!0},r),t(o)}))})(e).then((function(t){var n=function(n){if(!t[n].resolved){var l=e.resolver.depth;l>0&&l++,e.resolver.actions[l].push((function(){return function(e,t,n,r){var o=s.parse(n.source),l=n.source.split("\\").join("/").split("/");l.pop()||l.pop();var p="",d=t.split("#");d.length>1&&(p="#"+d[1],t=d[0]),l=l.join("/");var h,m,y,b=(h=s.parse(t).protocol,m=o.protocol,h&&h.length>2?h:m&&m.length>2?m:"file:");if(y="file:"===b?a.resolve(l?l+"/":"",t):s.resolve(l?l+"/":"",t),n.cache[y]){n.verbose&&console.warn("CACHED",y,p);var x=f(n.cache[y]),w=n.externalRef=x;if(p&&!1===(w=u(w,p))&&(w={},n.fatal)){var k=new Error("Cached $ref resolution failed "+y+p);if(!n.promise)throw k;n.promise.reject(k)}return w=g(w=v(w,x,t,p,y,n),n),r(f(w),y,n),Promise.resolve(w)}if(n.verbose&&console.warn("GET",y,p),n.handlers&&n.handlers[b])return n.handlers[b](l,t,p,n).then((function(e){return n.externalRef=e,e=g(e,n),n.cache[y]=e,r(e,y,n),e})).catch((function(e){throw n.verbose&&console.warn(e),e}));if(b&&b.startsWith("http")){var E=Object.assign({},n.fetchOptions,{agent:n.agent});return n.fetch(y,E).then((function(e){if(200!==e.status){if(n.ignoreIOErrors)return n.verbose&&console.warn("FAILED",t),n.externalRefs[t].failed=!0,'{"$ref":"'+t+'"}';throw new Error("Received status code ".concat(e.status,": ").concat(y))}return e.text()})).then((function(e){try{var o=c.parse(e,{schema:"core",prettyErrors:!0});if(e=n.externalRef=o,n.cache[y]=f(e),p&&!1===(e=u(e,p))&&(e={},n.fatal)){var i=new Error("Remote $ref resolution failed "+y+p);if(!n.promise)throw i;n.promise.reject(i)}e=g(e=v(e,o,t,p,y,n),n)}catch(e){if(n.verbose&&console.warn(e),!n.promise||!n.fatal)throw e;n.promise.reject(e)}return r(e,y,n),e})).catch((function(e){if(n.verbose&&console.warn(e),n.cache[y]={},!n.promise||!n.fatal)throw e;n.promise.reject(e)}))}var S='{"$ref":"'+t+'"}';return function(e,t,n,r,o){return new Promise((function(a,s){i.readFile(e,t,(function(e,t){e?n.ignoreIOErrors&&o?(n.verbose&&console.warn("FAILED",r),n.externalRefs[r].failed=!0,a(o)):s(e):a(t)}))}))}(y,n.encoding||"utf8",n,t,S).then((function(e){try{var o=c.parse(e,{schema:"core",prettyErrors:!0});if(e=n.externalRef=o,n.cache[y]=f(e),p&&!1===(e=u(e,p))&&(e={},n.fatal)){var i=new Error("File $ref resolution failed "+y+p);if(!n.promise)throw i;n.promise.reject(i)}e=g(e=v(e,o,t,p,y,n),n)}catch(e){if(n.verbose&&console.warn(e),!n.promise||!n.fatal)throw e;n.promise.reject(e)}return r(e,y,n),e})).catch((function(e){if(n.verbose&&console.warn(e),!n.promise||!n.fatal)throw e;n.promise.reject(e)}))}(e.openapi,n,e,(function(e,i,a){if(!t[n].resolved){var s={};s.context=t[n],s.$ref=n,s.original=f(e),s.updated=e,s.source=i,a.externals.push(s),t[n].resolved=!0}var l=Object.assign({},a,{source:"",resolver:{actions:a.resolver.actions,depth:a.resolver.actions.length-1,base:a.resolver.base}});a.patch&&t[n].description&&!e.description&&"object"==typeof e&&(e.description=t[n].description),t[n].data=e;var c,p=(c=t[n].paths,o(new Set(c)));p=p.sort((function(e,t){var n=e.startsWith("#/components/")||e.startsWith("#/definitions/"),r=t.startsWith("#/components/")||t.startsWith("#/definitions/");return n&&!r?-1:r&&!n?1:0}));var d,h=r(p);try{for(h.s();!(d=h.n()).done;){var m=d.value;if(t[n].resolvedAt&&m!==t[n].resolvedAt&&m.indexOf("x-ms-examples/")<0)a.verbose>1&&console.warn("Creating pointer to data at",m),u(a.openapi,m,{$ref:t[n].resolvedAt+t[n].extras[m],"x-miro":n+t[n].extras[m]});else{t[n].resolvedAt?a.verbose>1&&console.warn("Avoiding circular reference"):(t[n].resolvedAt=m,a.verbose>1&&console.warn("Creating initial clone of data at",m));var v=f(e);u(a.openapi,m,v)}}}catch(e){h.e(e)}finally{h.f()}0===a.resolver.actions[l.resolver.depth].length&&a.resolver.actions[l.resolver.depth].push((function(){return y(l)}))}))}))}};for(var l in t)n(l)})).catch((function(t){e.verbose&&console.warn(t),n(t)}));var l={options:e};l.actions=e.resolver.actions[e.resolver.depth],t(l)}))}function b(e,t,n){e.resolver.actions.push([]),y(e).then((function(r){var o;(o=r.actions,o.reduce((function(e,t){return e.then((function(e){return t().then(Array.prototype.concat.bind(e))}))}),Promise.resolve([]))).then((function(){if(e.resolver.depth>=e.resolver.actions.length)return console.warn("Ran off the end of resolver actions"),t(!0);e.resolver.depth++,e.resolver.actions[e.resolver.depth].length?setTimeout((function(){b(r.options,t,n)}),0):(e.verbose>1&&console.warn(m.colour.yellow+"Finished external resolution!",m.colour.normal),e.resolveInternal&&(e.verbose>1&&console.warn(m.colour.yellow+"Starting internal resolution!",m.colour.normal),e.openapi=d(e.openapi,e.original,{verbose:e.verbose-1}),e.verbose>1&&console.warn(m.colour.yellow+"Finished internal resolution!",m.colour.normal)),p(e.openapi,{},(function(t,n,r){h(t,n)&&(e.preserveMiro||delete t["x-miro"])})),t(e))})).catch((function(t){e.verbose&&console.warn(t),n(t)}))})).catch((function(t){e.verbose&&console.warn(t),n(t)}))}function x(e){if(e.cache||(e.cache={}),e.fetch||(e.fetch=l),e.source){var t=s.parse(e.source);(!t.protocol||t.protocol.length<=2)&&(e.source=a.resolve(e.source))}e.externals=[],e.externalRefs={},e.rewriteRefs=!0,e.resolver={},e.resolver.depth=0,e.resolver.base=e.source,e.resolver.actions=[[]]}e.exports={optionalResolve:function(e){return x(e),new Promise((function(t,n){e.resolve?b(e,t,n):t(e)}))},resolve:function(e,t,n){return n||(n={}),n.openapi=e,n.source=t,n.resolve=!0,x(n),new Promise((function(e,t){b(n,e,t)}))}}},6704:function(e,t,n){"use strict";function r(){return{depth:0,seen:new WeakMap,top:!0,combine:!1,allowRefSiblings:!1}}n(6992),n(1539),n(8783),n(4129),n(3948),n(2526),n(1817),n(9601),e.exports={getDefaultState:r,walkSchema:function e(t,n,o,i){if(void 0===o.depth&&(o=r()),null==t)return t;if(void 0!==t.$ref){var a={$ref:t.$ref};return o.allowRefSiblings&&t.description&&(a.description=t.description),i(a,n,o),a}if(o.combine&&(t.allOf&&Array.isArray(t.allOf)&&1===t.allOf.length&&delete(t=Object.assign({},t.allOf[0],t)).allOf,t.anyOf&&Array.isArray(t.anyOf)&&1===t.anyOf.length&&delete(t=Object.assign({},t.anyOf[0],t)).anyOf,t.oneOf&&Array.isArray(t.oneOf)&&1===t.oneOf.length&&delete(t=Object.assign({},t.oneOf[0],t)).oneOf),i(t,n,o),o.seen.has(t))return t;if("object"==typeof t&&null!==t&&o.seen.set(t,!0),o.top=!1,o.depth++,void 0!==t.items&&(o.property="items",e(t.items,t,o,i)),t.additionalItems&&"object"==typeof t.additionalItems&&(o.property="additionalItems",e(t.additionalItems,t,o,i)),t.additionalProperties&&"object"==typeof t.additionalProperties&&(o.property="additionalProperties",e(t.additionalProperties,t,o,i)),t.properties)for(var s in t.properties){var l=t.properties[s];o.property="properties/"+s,e(l,t,o,i)}if(t.patternProperties)for(var c in t.patternProperties){var u=t.patternProperties[c];o.property="patternProperties/"+c,e(u,t,o,i)}if(t.allOf)for(var p in t.allOf){var f=t.allOf[p];o.property="allOf/"+p,e(f,t,o,i)}if(t.anyOf)for(var d in t.anyOf){var h=t.anyOf[d];o.property="anyOf/"+d,e(h,t,o,i)}if(t.oneOf)for(var m in t.oneOf){var v=t.oneOf[m];o.property="oneOf/"+m,e(v,t,o,i)}return t.not&&(o.property="not",e(t.not,t,o,i)),o.depth--,t}}},4188:function(e,t,n){"use strict";var r=n(9713).default,o=n(319).default;n(9601),n(6992),n(1539),n(8783),n(4129),n(3948),n(489),n(2222),n(1249),n(7941),e.exports={nop:function(e){return e},clone:function(e){return JSON.parse(JSON.stringify(e))},shallowClone:function(e){var t={};for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t},deepClone:function e(t){var n=Array.isArray(t)?[]:{};for(var r in t)(t.hasOwnProperty(r)||Array.isArray(t))&&(n[r]="object"==typeof t[r]?e(t[r]):t[r]);return n},fastClone:function(e){return Object.assign({},e)},circularClone:function e(t,n){if(n||(n=new WeakMap),Object(t)!==t||t instanceof Function)return t;if(n.has(t))return n.get(t);try{var i=new t.constructor}catch(e){i=Object.create(Object.getPrototypeOf(t))}return n.set(t,i),Object.assign.apply(Object,[i].concat(o(Object.keys(t).map((function(o){return r({},o,e(t[o],n))})))))}}},3094:function(e,t,n){"use strict";n(7042),n(4916),n(3123);var r=n(9290).recurse,o=n(4188).shallowClone,i=n(2580).jptr,a=n(3856).isRef;e.exports={dereference:function e(t,n,s){s||(s={}),s.cache||(s.cache={}),s.state||(s.state={}),s.state.identityDetection=!0,s.depth=s.depth?s.depth+1:1;var l=s.depth>1?t:o(t),c={data:l},u=s.depth>1?n:o(n);s.master||(s.master=l);for(var p=function(e){return e&&e.verbose?{warn:function(){var e=Array.prototype.slice.call(arguments);console.warn.apply(console,e)}}:{warn:function(){}}}(s),f=1;f>0;)f=0,r(c,s.state,(function(t,n,r){if(a(t,n)){var o=t[n];if(f++,s.cache[o]){var l=s.cache[o];if(l.resolved)p.warn("Patching %s for %s",o,l.path),r.parent[r.pkey]=l.data,s.$ref&&"object"==typeof r.parent[r.pkey]&&(r.parent[r.pkey][s.$ref]=o);else{if(o===l.path)throw new Error("Tight circle at ".concat(l.path));p.warn("Unresolved ref"),r.parent[r.pkey]=i(l.source,l.path),!1===r.parent[r.pkey]&&(r.parent[r.pkey]=i(l.source,l.key)),s.$ref&&"object"==typeof r.parent[r.pkey]&&(r.parent[s.$ref]=o)}}else{var c={};c.path=r.path.split("/$ref")[0],c.key=o,p.warn("Dereffing %s at %s",o,c.path),c.source=u,c.data=i(c.source,c.key),!1===c.data&&(c.data=i(s.master,c.key),c.source=s.master),!1===c.data&&p.warn("Missing $ref target",c.key),s.cache[o]=c,c.data=r.parent[r.pkey]=e(i(c.source,c.key),c.source,s),s.$ref&&"object"==typeof r.parent[r.pkey]&&(r.parent[r.pkey][s.$ref]=o),c.resolved=!0}}}));return c.data}}},3856:function(e){"use strict";e.exports={isRef:function(e,t){return"$ref"===t&&!!e&&"string"==typeof e[t]}}},2580:function(e,t,n){"use strict";function r(e){return e.replace(/\~1/g,"/").replace(/~0/g,"~")}n(4916),n(5306),n(3123),n(9600),n(7042),n(6755),n(1539),n(9714),e.exports={jptr:function(e,t,n){if(void 0===e)return!1;if(!t||"string"!=typeof t||"#"===t)return void 0!==n?n:e;if(t.indexOf("#")>=0){var o=t.split("#");if(o[0])return!1;t=o[1],t=decodeURIComponent(t.slice(1).split("+").join(" "))}t.startsWith("/")&&(t=t.slice(1));for(var i=t.split("/"),a=0;a0?i[a-1]:"",-1!=l||e&&e.hasOwnProperty(i[a]))if(l>=0)s&&(e[l]=n),e=e[l];else{if(-2===l)return s?(Array.isArray(e)&&e.push(n),n):void 0;s&&(e[i[a]]=n),e=e[i[a]]}else{if(void 0===n||"object"!=typeof e||Array.isArray(e))return!1;e[i[a]]=s?n:"0"===i[a+1]||"-"===i[a+1]?[]:{},e=e[i[a]]}}return e},jpescape:function(e){return e.replace(/\~/g,"~0").replace(/\//g,"~1")},jpunescape:r}},9290:function(e,t,n){"use strict";n(6992),n(1539),n(8783),n(4129),n(3948),n(9601);var r=n(2580).jpescape;e.exports={recurse:function e(t,n,o){if(n||(n={depth:0}),n.depth||(n=Object.assign({},{path:"#",depth:0,pkey:"",parent:{},payload:{},seen:new WeakMap,identity:!1,identityDetection:!1},n)),"object"==typeof t){var i=n.path;for(var a in t){if(n.key=a,n.path=n.path+"/"+encodeURIComponent(r(a)),n.identityPath=n.seen.get(t[a]),n.identity=void 0!==n.identityPath,t.hasOwnProperty(a)&&o(t,a,n),"object"==typeof t[a]&&!n.identity){n.identityDetection&&!Array.isArray(t[a])&&null!==t[a]&&n.seen.set(t[a],n.path);var s={};s.parent=t,s.path=n.path,s.depth=n.depth?n.depth+1:1,s.pkey=a,s.payload=n.payload,s.seen=n.seen,s.identity=!1,s.identityDetection=n.identityDetection,e(t[a],s,o)}n.path=i}}}}},6399:function(e,t,n){"use strict";var r=n(3269).default,o=n(4575).default,i=n(2205).default,a=n(9842).default,s=n(5957).default;n(8309),n(7941),n(2222),n(6755),n(4916),n(5306),n(3123),n(9600),n(3210),n(7327),n(2526),n(1817),n(9601),n(9826),n(1539),n(9714),n(6992),n(8783),n(3948),n(285),n(8478),n(8674),n(4723),n(4747);var l,c=n(9305),u=n(8575),p=(n(6470),n(4480)),f=n(7707),d=n(7707),h=n(2580),m=h.jptr,v=n(3856).isRef,g=n(4188).clone,y=n(4188).circularClone,b=n(9290).recurse,x=n(1338),w=n(6704),k=n(8276),E=n(873).statusCodes,S=n(8500).i8,_="3.0.0",O=function(e){i(n,e);var t=a(n);function n(e){var r;return o(this,n),(r=t.call(this,e)).name="S2OError",r}return n}(s(Error));function A(e,t){var n=new O(e);if(n.options=t,!t.promise)throw n;t.promise.reject(n)}function I(e,t,n){n.warnOnly?t[n.warnProperty||"x-s2o-warning"]=e:A(e,n)}function C(e,t){w.walkSchema(e,{},{},(function(e,n,o){!function(e,t){if(e["x-required"]&&Array.isArray(e["x-required"])&&(e.required||(e.required=[]),e.required=e.required.concat(e["x-required"]),delete e["x-required"]),e["x-anyOf"]&&(e.anyOf=e["x-anyOf"],delete e["x-anyOf"]),e["x-oneOf"]&&(e.oneOf=e["x-oneOf"],delete e["x-oneOf"]),e["x-not"]&&(e.not=e["x-not"],delete e["x-not"]),"boolean"==typeof e["x-nullable"]&&(e.nullable=e["x-nullable"],delete e["x-nullable"]),"object"==typeof e["x-discriminator"]&&"string"==typeof e["x-discriminator"].propertyName)for(var n in e.discriminator=e["x-discriminator"],delete e["x-discriminator"],e.discriminator.mapping){var r=e.discriminator.mapping[n];r.startsWith("#/definitions/")&&(e.discriminator.mapping[n]=r.replace("#/definitions/","#/components/schemas/"))}}(e),function(e,t,n){if(e.nullable&&n.patches++,e.discriminator&&"string"==typeof e.discriminator&&(e.discriminator={propertyName:e.discriminator}),e.items&&Array.isArray(e.items)&&(0===e.items.length?e.items={}:1===e.items.length?e.items=e.items[0]:e.items={anyOf:e.items}),e.type&&Array.isArray(e.type))if(n.patch){if(n.patches++,0===e.type.length)delete e.type;else{e.oneOf||(e.oneOf=[]);var o,i=r(e.type);try{for(i.s();!(o=i.n()).done;){var a=o.value,s={};if("null"===a)e.nullable=!0;else{s.type=a;var l,c=r(k.arrayProperties);try{for(c.s();!(l=c.n()).done;){var u=l.value;void 0!==e.prop&&(s[u]=e[u],delete e[u])}}catch(e){c.e(e)}finally{c.f()}}s.type&&e.oneOf.push(s)}}catch(e){i.e(e)}finally{i.f()}delete e.type,0===e.oneOf.length?delete e.oneOf:e.oneOf.length<2&&(e.type=e.oneOf[0].type,Object.keys(e.oneOf[0]).length>1&&I("Lost properties from oneOf",e,n),delete e.oneOf)}e.type&&Array.isArray(e.type)&&1===e.type.length&&(e.type=e.type[0])}else A("(Patchable) schema type must not be an array",n);e.type&&"null"===e.type&&(delete e.type,e.nullable=!0),"array"!==e.type||e.items||(e.items={}),"file"===e.type&&(e.type="string",e.format="binary"),"boolean"==typeof e.required&&(e.required&&e.name&&(void 0===t.required&&(t.required=[]),Array.isArray(t.required)&&t.required.push(e.name)),delete e.required),e.xml&&"string"==typeof e.xml.namespace&&(e.xml.namespace||delete e.xml.namespace),void 0!==e.allowEmptyValue&&(n.patches++,delete e.allowEmptyValue)}(e,n,t)}))}function T(e,t,n){var r=n.payload.options;if(v(e,t)){if(e[t].startsWith("#/components/"));else if("#/consumes"===e[t])delete e[t],n.parent[n.pkey]=g(r.openapi.consumes);else if("#/produces"===e[t])delete e[t],n.parent[n.pkey]=g(r.openapi.produces);else if(e[t].startsWith("#/definitions/")){var o=e[t].replace("#/definitions/","").split("/"),i=h.jpunescape(o[0]),a=l.schemas[decodeURIComponent(i)];a?o[0]=a:I("Could not resolve reference "+e[t],e,r),e[t]="#/components/schemas/"+o.join("/")}else if(e[t].startsWith("#/parameters/"))e[t]="#/components/parameters/"+k.sanitise(e[t].replace("#/parameters/",""));else if(e[t].startsWith("#/responses/"))e[t]="#/components/responses/"+k.sanitise(e[t].replace("#/responses/",""));else if(e[t].startsWith("#")){var s=g(h.jptr(r.openapi,e[t]));if(!1===s)I("direct $ref not found "+e[t],e,r);else if(r.refmap[e[t]])e[t]=r.refmap[e[t]];else{var c=e[t],u="schemas",p=(c=(c=(c=(c=c.replace("/properties/headers/","")).replace("/properties/responses/","")).replace("/properties/parameters/","")).replace("/properties/schemas/","")).lastIndexOf("/schema");if("schemas"==(u=c.indexOf("/headers/")>p?"headers":c.indexOf("/responses/")>p?"responses":c.indexOf("/example")>p?"examples":c.indexOf("/x-")>p?"extensions":c.indexOf("/parameters/")>p?"parameters":"schemas")&&C(s,r),"responses"!==u&&"extensions"!==u){var f=u.substr(0,u.length-1);"parameter"===f&&s.name&&s.name===k.sanitise(s.name)&&(f=encodeURIComponent(s.name));var d=1;for(e["x-miro"]&&(f=function(e){return e=e.indexOf("#")>=0?e.split("#")[1].split("/").pop():e.split("/").pop().split(".")[0],encodeURIComponent(k.sanitise(e))}(e["x-miro"]),d="");h.jptr(r.openapi,"#/components/"+u+"/"+f+d);)d=""===d?2:++d;var m="#/components/"+u+"/"+f+d,y="";"examples"===u&&(s={value:s},y="/value"),h.jptr(r.openapi,m,s),r.refmap[e[t]]=m+y,e[t]=m+y}}}if(delete e["x-miro"],Object.keys(e).length>1){var b=e[t],x=n.path.indexOf("/schema")>=0;"preserve"===r.refSiblings||(x&&"allOf"===r.refSiblings?(delete e.$ref,n.parent[n.pkey]={allOf:[{$ref:b},e]}):n.parent[n.pkey]={$ref:b})}}if("x-ms-odata"===t&&"string"==typeof e[t]&&e[t].startsWith("#/")){var w=e[t].replace("#/definitions/","").replace("#/components/schemas/","").split("/"),E=l.schemas[decodeURIComponent(w[0])];E?w[0]=E:I("Could not resolve reference "+e[t],e,r),e[t]="#/components/schemas/"+w.join("/")}}function R(e){for(var t in e)for(var n in e[t]){var r=k.sanitise(n);n!==r&&(e[t][r]=e[t][n],delete e[t][n])}}function P(e,t){if("basic"===e.type&&(e.type="http",e.scheme="basic"),"oauth2"===e.type){var n={},r=e.flow;"application"===e.flow&&(r="clientCredentials"),"accessCode"===e.flow&&(r="authorizationCode"),void 0!==e.authorizationUrl&&(n.authorizationUrl=e.authorizationUrl.split("?")[0].trim()||"/"),"string"==typeof e.tokenUrl&&(n.tokenUrl=e.tokenUrl.split("?")[0].trim()||"/"),n.scopes=e.scopes||{},e.flows={},e.flows[r]=n,delete e.flow,delete e.authorizationUrl,delete e.tokenUrl,delete e.scopes,void 0!==e.name&&(t.patch?(t.patches++,delete e.name):A("(Patchable) oauth2 securitySchemes should not have name property",t))}}function j(e){return e&&!e["x-s2o-delete"]}function L(e,t){if(e.$ref)e.$ref=e.$ref.replace("#/responses/","#/components/responses/");else{e.type&&!e.schema&&(e.schema={}),e.type&&(e.schema.type=e.type),e.items&&"array"!==e.items.type&&(e.items.collectionFormat!==e.collectionFormat&&I("Nested collectionFormats are not supported",e,t),delete e.items.collectionFormat),"array"===e.type?("ssv"===e.collectionFormat?I("collectionFormat:ssv is no longer supported for headers",e,t):"pipes"===e.collectionFormat?I("collectionFormat:pipes is no longer supported for headers",e,t):"multi"===e.collectionFormat?e.explode=!0:"tsv"===e.collectionFormat?(I("collectionFormat:tsv is no longer supported",e,t),e["x-collectionFormat"]="tsv"):e.style="simple",delete e.collectionFormat):e.collectionFormat&&(t.patch?(t.patches++,delete e.collectionFormat):A("(Patchable) collectionFormat is only applicable to header.type array",t)),delete e.type;var n,o=r(k.parameterTypeProperties);try{for(o.s();!(n=o.n()).done;){var i=n.value;void 0!==e[i]&&(e.schema[i]=e[i],delete e[i])}}catch(e){o.e(e)}finally{o.f()}var a,s=r(k.arrayProperties);try{for(s.s();!(a=s.n()).done;){var l=a.value;void 0!==e[l]&&(e.schema[l]=e[l],delete e[l])}}catch(e){s.e(e)}finally{s.f()}}}function N(e,t){if(e.$ref.indexOf("#/parameters/")>=0){var n=e.$ref.split("#/parameters/");e.$ref=n[0]+"#/components/parameters/"+k.sanitise(n[1])}e.$ref.indexOf("#/definitions/")>=0&&I("Definition used as parameter",e,t)}function M(e,t,n,o,i,a,s){var l,c={},u=!0;if(t&&t.consumes&&"string"==typeof t.consumes){if(!s.patch)return A("(Patchable) operation.consumes must be an array",s);s.patches++,t.consumes=[t.consumes]}Array.isArray(a.consumes)||delete a.consumes;var p=((t?t.consumes:null)||a.consumes||[]).filter(k.uniqueOnly);if(e&&e.$ref&&"string"==typeof e.$ref){N(e,s);var f=decodeURIComponent(e.$ref.replace("#/components/parameters/","")),d=!1,h=a.components.parameters[f];if(h&&!h["x-s2o-delete"]||!e.$ref.startsWith("#/")||(e["x-s2o-delete"]=!0,d=!0),d){var v=e.$ref,y=m(a,e.$ref);!y&&v.startsWith("#/")?I("Could not resolve reference "+v,e,s):y&&(e=y)}}if(e&&(e.name||e.in)){"boolean"==typeof e["x-deprecated"]&&(e.deprecated=e["x-deprecated"],delete e["x-deprecated"]),void 0!==e["x-example"]&&(e.example=e["x-example"],delete e["x-example"]),"body"===e.in||e.type||(s.patch?(s.patches++,e.type="string"):A("(Patchable) parameter.type is mandatory for non-body parameters",s)),e.type&&"object"==typeof e.type&&e.type.$ref&&(e.type=m(a,e.type.$ref)),"file"===e.type&&(e["x-s2o-originalType"]=e.type,l=e.type),e.description&&"object"==typeof e.description&&e.description.$ref&&(e.description=m(a,e.description.$ref)),null===e.description&&delete e.description;var x=e.collectionFormat;if("array"!==e.type||x||(x="csv"),x&&("array"!==e.type&&(s.patch?(s.patches++,delete e.collectionFormat):A("(Patchable) collectionFormat is only applicable to param.type array",s)),"csv"!==x||"query"!==e.in&&"cookie"!==e.in||(e.style="form",e.explode=!1),"csv"!==x||"path"!==e.in&&"header"!==e.in||(e.style="simple"),"ssv"===x&&("query"===e.in?e.style="spaceDelimited":I("collectionFormat:ssv is no longer supported except for in:query parameters",e,s)),"pipes"===x&&("query"===e.in?e.style="pipeDelimited":I("collectionFormat:pipes is no longer supported except for in:query parameters",e,s)),"multi"===x&&(e.explode=!0),"tsv"===x&&(I("collectionFormat:tsv is no longer supported",e,s),e["x-collectionFormat"]="tsv"),delete e.collectionFormat),e.type&&"body"!==e.type&&"formData"!==e.in)if(e.items&&e.schema)I("parameter has array,items and schema",e,s);else{e.schema&&s.patches++,e.schema&&"object"==typeof e.schema||(e.schema={}),e.schema.type=e.type,e.items&&(e.schema.items=e.items,delete e.items,b(e.schema.items,null,(function(t,n,r){"collectionFormat"===n&&"string"==typeof t[n]&&(x&&t[n]!==x&&I("Nested collectionFormats are not supported",e,s),delete t[n])})));var w,E=r(k.parameterTypeProperties);try{for(E.s();!(w=E.n()).done;){var S=w.value;void 0!==e[S]&&(e.schema[S]=e[S]),delete e[S]}}catch(e){E.e(e)}finally{E.f()}}e.schema&&C(e.schema,s),e["x-ms-skip-url-encoding"]&&"query"===e.in&&(e.allowReserved=!0,delete e["x-ms-skip-url-encoding"])}if(e&&"formData"===e.in){u=!1,c.content={};var _="application/x-www-form-urlencoded";if(p.length&&p.indexOf("multipart/form-data")>=0&&(_="multipart/form-data"),c.content[_]={},e.schema)c.content[_].schema=e.schema,e.schema.$ref&&(c["x-s2o-name"]=decodeURIComponent(e.schema.$ref.replace("#/components/schemas/","")));else{c.content[_].schema={},c.content[_].schema.type="object",c.content[_].schema.properties={},c.content[_].schema.properties[e.name]={};var O=c.content[_].schema,T=c.content[_].schema.properties[e.name];e.description&&(T.description=e.description),e.example&&(T.example=e.example),e.type&&(T.type=e.type);var R,P=r(k.parameterTypeProperties);try{for(P.s();!(R=P.n()).done;){var j=R.value;void 0!==e[j]&&(T[j]=e[j])}}catch(e){P.e(e)}finally{P.f()}!0===e.required&&(O.required||(O.required=[]),O.required.push(e.name),c.required=!0),void 0!==e.default&&(T.default=e.default),T.properties&&(T.properties=e.properties),e.allOf&&(T.allOf=e.allOf),"array"===e.type&&e.items&&(T.items=e.items,T.items.collectionFormat&&delete T.items.collectionFormat),"file"!==l&&"file"!==e["x-s2o-originalType"]||(T.type="string",T.format="binary"),D(e,T)}}else e&&"file"===e.type&&(e.required&&(c.required=e.required),c.content={},c.content["application/octet-stream"]={},c.content["application/octet-stream"].schema={},c.content["application/octet-stream"].schema.type="string",c.content["application/octet-stream"].schema.format="binary",D(e,c));if(e&&"body"===e.in){c.content={},e.name&&(c["x-s2o-name"]=(t&&t.operationId?k.sanitiseAll(t.operationId):"")+("_"+e.name).toCamelCase()),e.description&&(c.description=e.description),e.required&&(c.required=e.required),t&&s.rbname&&e.name&&(t[s.rbname]=e.name),e.schema&&e.schema.$ref?c["x-s2o-name"]=decodeURIComponent(e.schema.$ref.replace("#/components/schemas/","")):e.schema&&"array"===e.schema.type&&e.schema.items&&e.schema.items.$ref&&(c["x-s2o-name"]=decodeURIComponent(e.schema.items.$ref.replace("#/components/schemas/",""))+"Array"),p.length||p.push("application/json");var L,M=r(p);try{for(M.s();!(L=M.n()).done;){var F=L.value;c.content[F]={},c.content[F].schema=g(e.schema||{}),C(c.content[F].schema,s)}}catch(e){M.e(e)}finally{M.f()}D(e,c)}if(Object.keys(c).length>0&&(e["x-s2o-delete"]=!0,t&&(t.requestBody&&u?(t.requestBody["x-s2o-overloaded"]=!0,I("Operation "+(t.operationId||i)+" has multiple requestBodies",t,s)):(t.requestBody||(t=n[o]=function(e,t){for(var n={},r=0,o=Object.keys(e);r=0?I("definition used as response: "+e.$ref,e,i):e.$ref.startsWith("#/responses/")&&(e.$ref="#/components/responses/"+k.sanitise(decodeURIComponent(e.$ref.replace("#/responses/",""))));else{if((void 0===e.description||null===e.description||""===e.description&&i.patch)&&(i.patch?"object"!=typeof e||Array.isArray(e)||(i.patches++,e.description=E[e]||""):A("(Patchable) response.description is mandatory",i)),void 0!==e.schema){if(C(e.schema,i),e.schema.$ref&&"string"==typeof e.schema.$ref&&e.schema.$ref.startsWith("#/responses/")&&(e.schema.$ref="#/components/responses/"+k.sanitise(decodeURIComponent(e.schema.$ref.replace("#/responses/","")))),n&&n.produces&&"string"==typeof n.produces){if(!i.patch)return A("(Patchable) operation.produces must be an array",i);i.patches++,n.produces=[n.produces]}o.produces&&!Array.isArray(o.produces)&&delete o.produces;var a=((n?n.produces:null)||o.produces||[]).filter(k.uniqueOnly);a.length||a.push("*/*"),e.content={};var s,l=r(a);try{for(l.s();!(s=l.n()).done;){var c=s.value;if(e.content[c]={},e.content[c].schema=g(e.schema),e.examples&&e.examples[c]){var u={};u.value=e.examples[c],e.content[c].examples={},e.content[c].examples.response=u,delete e.examples[c]}"file"===e.content[c].schema.type&&(e.content[c].schema={type:"string",format:"binary"})}}catch(e){l.e(e)}finally{l.f()}delete e.schema}for(var p in e.examples)e.content||(e.content={}),e.content[p]||(e.content[p]={}),e.content[p].examples={},e.content[p].examples.response={},e.content[p].examples.response.value=e.examples[p];if(delete e.examples,e.headers)for(var f in e.headers)"status code"===f.toLowerCase()?i.patch?(i.patches++,delete e.headers[f]):A('(Patchable) "Status Code" is not a valid header',i):L(e.headers[f],i)}}function z(e,t,n,o,i){for(var a in e){var s=e[a];for(var l in s&&s["x-trace"]&&"object"==typeof s["x-trace"]&&(s.trace=s["x-trace"],delete s["x-trace"]),s&&s["x-summary"]&&"string"==typeof s["x-summary"]&&(s.summary=s["x-summary"],delete s["x-summary"]),s&&s["x-description"]&&"string"==typeof s["x-description"]&&(s.description=s["x-description"],delete s["x-description"]),s&&s["x-servers"]&&Array.isArray(s["x-servers"])&&(s.servers=s["x-servers"],delete s["x-servers"]),s)if(k.httpMethods.indexOf(l)>=0||"x-amazon-apigateway-any-method"===l){var c=s[l];if(c&&c.parameters&&Array.isArray(c.parameters)){if(s.parameters){var p,f=r(s.parameters);try{var d=function(){var e=p.value;"string"==typeof e.$ref&&(N(e,n),e=m(i,e.$ref)),c.parameters.find((function(t,n,r){return t.name===e.name&&t.in===e.in}))||"formData"!==e.in&&"body"!==e.in&&"file"!==e.type||(c=M(e,c,s,l,a,i,n),n.rbname&&""===c[n.rbname]&&delete c[n.rbname])};for(f.s();!(p=f.n()).done;)d()}catch(e){f.e(e)}finally{f.f()}}var v,y=r(c.parameters);try{for(y.s();!(v=y.n()).done;){var b=v.value;c=M(b,c,s,l,l+":"+a,i,n)}}catch(e){y.e(e)}finally{y.f()}n.rbname&&""===c[n.rbname]&&delete c[n.rbname],n.debug||c.parameters&&(c.parameters=c.parameters.filter(j))}if(c&&c.security&&R(c.security),"object"==typeof c){if(!c.responses){c.responses={default:{description:"Default response"}}}for(var x in c.responses)F(c.responses[x],0,c,i,n)}if(c&&c["x-servers"]&&Array.isArray(c["x-servers"]))c.servers=c["x-servers"],delete c["x-servers"];else if(c&&c.schemes&&c.schemes.length){var w,E=r(c.schemes);try{for(E.s();!(w=E.n()).done;){var S=w.value;if((!i.schemes||i.schemes.indexOf(S)<0)&&(c.servers||(c.servers=[]),Array.isArray(i.servers))){var _,O=r(i.servers);try{for(O.s();!(_=O.n()).done;){var A=_.value,I=g(A),C=u.parse(I.url);C.protocol=S,I.url=C.format(),c.servers.push(I)}}catch(e){O.e(e)}finally{O.f()}}}}catch(e){E.e(e)}finally{E.f()}}if(n.debug&&(c["x-s2o-consumes"]=c.consumes||[],c["x-s2o-produces"]=c.produces||[]),c){if(delete c.consumes,delete c.produces,delete c.schemes,c["x-ms-examples"]){for(var T in c["x-ms-examples"]){var P=c["x-ms-examples"][T],L=k.sanitiseAll(T);if(P.parameters)for(var D in P.parameters){var z,U=P.parameters[D],B=r((c.parameters||[]).concat(s.parameters||[]));try{for(B.s();!(z=B.n()).done;){var $=z.value;$.$ref&&($=h.jptr(i,$.$ref)),$.name!==D||$.example||($.examples||($.examples={}),$.examples[T]={value:U})}}catch(e){B.e(e)}finally{B.f()}}if(P.responses)for(var q in P.responses){if(P.responses[q].headers)for(var V in P.responses[q].headers){var W=P.responses[q].headers[V];for(var H in c.responses[q].headers)H===V&&(c.responses[q].headers[H].example=W)}if(P.responses[q].body&&(i.components.examples[L]={value:g(P.responses[q].body)},c.responses[q]&&c.responses[q].content))for(var Y in c.responses[q].content){var G=c.responses[q].content[Y];G.examples||(G.examples={}),G.examples[T]={$ref:"#/components/examples/"+L}}}}delete c["x-ms-examples"]}if(c.parameters&&0===c.parameters.length&&delete c.parameters,c.requestBody){var Q=c.operationId?k.sanitiseAll(c.operationId):k.sanitiseAll(l+a).toCamelCase(),X=k.sanitise(c.requestBody["x-s2o-name"]||Q||"");delete c.requestBody["x-s2o-name"];var K=JSON.stringify(c.requestBody),Z=k.hash(K);if(!o[Z]){var J={};J.name=X,J.body=c.requestBody,J.refs=[],o[Z]=J}var ee="#/"+t+"/"+encodeURIComponent(h.jpescape(a))+"/"+l+"/requestBody";o[Z].refs.push(ee)}}}if(s&&s.parameters){for(var te in s.parameters)M(s.parameters[te],null,s,null,a,i,n);!n.debug&&Array.isArray(s.parameters)&&(s.parameters=s.parameters.filter(j))}}}function U(e){return e&&e.url&&"string"==typeof e.url?(e.url=e.url.split("{{").join("{"),e.url=e.url.split("}}").join("}"),e.url.replace(/\{(.+?)\}/g,(function(t,n){e.variables||(e.variables={}),e.variables[n]={default:"unknown"}})),e):e}function B(e,t,n){if(void 0===e.info||null===e.info){if(!t.patch)return n(new O("(Patchable) info object is mandatory"));t.patches++,e.info={version:"",title:""}}if("object"!=typeof e.info||Array.isArray(e.info))return n(new O("info must be an object"));if(void 0===e.info.title||null===e.info.title){if(!t.patch)return n(new O("(Patchable) info.title cannot be null"));t.patches++,e.info.title=""}if(void 0===e.info.version||null===e.info.version){if(!t.patch)return n(new O("(Patchable) info.version cannot be null"));t.patches++,e.info.version=""}if("string"!=typeof e.info.version){if(!t.patch)return n(new O("(Patchable) info.version must be a string"));t.patches++,e.info.version=e.info.version.toString()}if(void 0!==e.info.logo){if(!t.patch)return n(new O("(Patchable) info should not have logo property"));t.patches++,e.info["x-logo"]=e.info.logo,delete e.info.logo}if(void 0!==e.info.termsOfService){if(null===e.info.termsOfService){if(!t.patch)return n(new O("(Patchable) info.termsOfService cannot be null"));t.patches++,e.info.termsOfService=""}try{new URL(e.info.termsOfService)}catch(r){if(!t.patch)return n(new O("(Patchable) info.termsOfService must be a URL"));t.patches++,delete e.info.termsOfService}}}function $(e,t,n){if(void 0===e.paths){if(!t.patch)return n(new O("(Patchable) paths object is mandatory"));t.patches++,e.paths={}}}function q(e,t,n){return p(n,new Promise((function(n,o){if(e||(e={}),t.original=e,t.text||(t.text=d.stringify(e)),t.externals=[],t.externalRefs={},t.rewriteRefs=!0,t.preserveMiro=!0,t.promise={},t.promise.resolve=n,t.promise.reject=o,t.patches=0,t.cache||(t.cache={}),t.source&&(t.cache[t.source]=t.original),function(e,t){var n=new WeakSet;b(e,{identityDetection:!0},(function(e,r,o){"object"==typeof e[r]&&null!==e[r]&&(n.has(e[r])?t.anchors?e[r]=g(e[r]):A("YAML anchor or merge key at "+o.path,t):n.add(e[r]))}))}(e,t),e.openapi&&"string"==typeof e.openapi&&e.openapi.startsWith("3."))return t.openapi=y(e),B(t.openapi,t,o),$(t.openapi,t,o),void x.optionalResolve(t).then((function(){return t.direct?n(t.openapi):n(t)})).catch((function(e){console.warn(e),o(e)}));if(!e.swagger||"2.0"!=e.swagger)return o(new O("Unsupported swagger/OpenAPI version: "+(e.openapi?e.openapi:e.swagger)));var i=t.openapi={};if(i.openapi="string"==typeof t.targetVersion&&t.targetVersion.startsWith("3.")?t.targetVersion:_,t.origin){i["x-origin"]||(i["x-origin"]=[]);var a={};a.url=t.source||t.origin,a.format="swagger",a.version=e.swagger,a.converter={},a.converter.url="https://github.com/mermade/oas-kit",a.converter.version=S,i["x-origin"].push(a)}if(delete(i=Object.assign(i,y(e))).swagger,b(i,{},(function(e,t,n){null===e[t]&&!t.startsWith("x-")&&"default"!==t&&n.path.indexOf("/example")<0&&delete e[t]})),e.host){var s,c=r(Array.isArray(e.schemes)?e.schemes:[""]);try{for(c.s();!(s=c.n()).done;){var u=s.value,p={},f=(e.basePath||"").replace(/\/$/,"");p.url=(u?u+":":"")+"//"+e.host+f,U(p),i.servers||(i.servers=[]),i.servers.push(p)}}catch(e){c.e(e)}finally{c.f()}}else if(e.basePath){var v={};v.url=e.basePath,U(v),i.servers||(i.servers=[]),i.servers.push(v)}if(delete i.host,delete i.basePath,i["x-servers"]&&Array.isArray(i["x-servers"])&&(i.servers=i["x-servers"],delete i["x-servers"]),e["x-ms-parameterized-host"]){var w=e["x-ms-parameterized-host"],E={};E.url=w.hostTemplate+(e.basePath?e.basePath:""),E.variables={};var I=E.url.match(/\{\w+\}/g);for(var j in w.parameters){var N=w.parameters[j];N.$ref&&(N=g(m(i,N.$ref))),j.startsWith("x-")||(delete N.required,delete N.type,delete N.in,void 0===N.default&&(N.enum?N.default=N.enum[0]:N.default="none"),N.name||(N.name=I[j].replace("{","").replace("}","")),E.variables[N.name]=N,delete N.name)}i.servers||(i.servers=[]),!1===w.useSchemePrefix?i.servers.push(E):e.schemes.forEach((function(e){i.servers.push(Object.assign({},E,{url:e+"://"+E.url}))})),delete i["x-ms-parameterized-host"]}B(i,t,o),$(i,t,o),"string"==typeof i.consumes&&(i.consumes=[i.consumes]),"string"==typeof i.produces&&(i.produces=[i.produces]),i.components={},i["x-callbacks"]&&(i.components.callbacks=i["x-callbacks"],delete i["x-callbacks"]),i.components.examples={},i.components.headers={},i["x-links"]&&(i.components.links=i["x-links"],delete i["x-links"]),i.components.parameters=i.parameters||{},i.components.responses=i.responses||{},i.components.requestBodies={},i.components.securitySchemes=i.securityDefinitions||{},i.components.schemas=i.definitions||{},delete i.definitions,delete i.responses,delete i.parameters,delete i.securityDefinitions,x.optionalResolve(t).then((function(){(function(e,t){var n={};for(var r in l={schemas:{}},e.security&&R(e.security),e.components.securitySchemes){var o=k.sanitise(r);r!==o&&(e.components.securitySchemes[o]&&A("Duplicate sanitised securityScheme name "+o,t),e.components.securitySchemes[o]=e.components.securitySchemes[r],delete e.components.securitySchemes[r]),P(e.components.securitySchemes[o],t)}for(var i in e.components.schemas){var a=k.sanitiseAll(i),s="";if(i!==a){for(;e.components.schemas[a+s];)s=s?++s:2;e.components.schemas[a+s]=e.components.schemas[i],delete e.components.schemas[i]}l.schemas[i]=a+s,C(e.components.schemas[a+s],t)}for(var c in t.refmap={},b(e,{payload:{options:t}},T),function(e,t){for(var n in t.refmap)h.jptr(e,n,{$ref:t.refmap[n]})}(e,t),e.components.parameters){var u=k.sanitise(c);c!==u&&(e.components.parameters[u]&&A("Duplicate sanitised parameter name "+u,t),e.components.parameters[u]=e.components.parameters[c],delete e.components.parameters[c]),M(e.components.parameters[u],null,null,null,u,e,t)}for(var p in e.components.responses){var f=k.sanitise(p);p!==f&&(e.components.responses[f]&&A("Duplicate sanitised response name "+f,t),e.components.responses[f]=e.components.responses[p],delete e.components.responses[p]);var d=e.components.responses[f];if(F(d,0,null,e,t),d.headers)for(var m in d.headers)"status code"===m.toLowerCase()?t.patch?(t.patches++,delete d.headers[m]):A('(Patchable) "Status Code" is not a valid header',t):L(d.headers[m],t)}for(var v in e.components.requestBodies){var y=e.components.requestBodies[v],x=JSON.stringify(y),w=k.hash(x),E={};E.name=v,E.body=y,E.refs=[],n[w]=E}if(z(e.paths,"paths",t,n,e),e["x-ms-paths"]&&z(e["x-ms-paths"],"x-ms-paths",t,n,e),!t.debug)for(var S in e.components.parameters)e.components.parameters[S]["x-s2o-delete"]&&delete e.components.parameters[S];t.debug&&(e["x-s2o-consumes"]=e.consumes||[],e["x-s2o-produces"]=e.produces||[]),delete e.consumes,delete e.produces,delete e.schemes;var _=[];if(e.components.requestBodies={},!t.resolveInternal){var O=1;for(var I in n){var j=n[I];if(j.refs.length>1){var N="";for(j.name||(j.name="requestBody",N=O++);_.indexOf(j.name+N)>=0;)N=N?++N:2;for(var D in j.name=j.name+N,_.push(j.name),e.components.requestBodies[j.name]=g(j.body),j.refs){var U={};U.$ref="#/components/requestBodies/"+j.name,h.jptr(e,j.refs[D],U)}}}}e.components.responses&&0===Object.keys(e.components.responses).length&&delete e.components.responses,e.components.parameters&&0===Object.keys(e.components.parameters).length&&delete e.components.parameters,e.components.examples&&0===Object.keys(e.components.examples).length&&delete e.components.examples,e.components.requestBodies&&0===Object.keys(e.components.requestBodies).length&&delete e.components.requestBodies,e.components.securitySchemes&&0===Object.keys(e.components.securitySchemes).length&&delete e.components.securitySchemes,e.components.headers&&0===Object.keys(e.components.headers).length&&delete e.components.headers,e.components.schemas&&0===Object.keys(e.components.schemas).length&&delete e.components.schemas,e.components&&0===Object.keys(e.components).length&&delete e.components})(t.openapi,t),t.direct?n(t.openapi):n(t)})).catch((function(e){console.warn(e),o(e)}))})))}function V(e,t,n){return p(n,new Promise((function(n,r){var o=null,i=null;try{o=JSON.parse(e),t.text=JSON.stringify(o,null,2)}catch(n){i=n;try{o=d.parse(e,{schema:"core",prettyErrors:!0}),t.sourceYaml=!0,t.text=e}catch(e){i=e}}o?q(o,t).then((function(e){return n(e)})).catch((function(e){return r(e)})):r(new O(i?i.message:"Could not parse string"))})))}e.exports={S2OError:O,targetVersion:_,convert:q,convertObj:q,convertUrl:function(e,t,n){return p(n,new Promise((function(n,r){t.origin=!0,t.source||(t.source=e),t.verbose&&console.warn("GET "+e),t.fetch||(t.fetch=f);var o=Object.assign({},t.fetchOptions,{agent:t.agent});t.fetch(e,o).then((function(t){if(200!==t.status)throw new O("Received status code ".concat(t.status,": ").concat(e));return t.text()})).then((function(e){V(e,t).then((function(e){return n(e)})).catch((function(e){return r(e)}))})).catch((function(e){r(e)}))})))},convertStr:V,convertFile:function(e,t,n){return p(n,new Promise((function(n,r){c.readFile(e,t.encoding||"utf8",(function(o,i){o?r(o):(t.sourceFile=e,V(i,t).then((function(e){return n(e)})).catch((function(e){return r(e)})))}))})))},convertStream:function(e,t,n){return p(n,new Promise((function(n,r){var o="";e.on("data",(function(e){o+=e})).on("end",(function(){V(o,t).then((function(e){return n(e)})).catch((function(e){return r(e)}))}))})))}}},873:function(e,t,n){"use strict";n(9601);var r=n(6177);e.exports={statusCodes:Object.assign({},{default:"Default response","1XX":"Informational",103:"Early hints","2XX":"Successful","3XX":"Redirection","4XX":"Client Error","5XX":"Server Error","7XX":"Developer Error"},r.STATUS_CODES)}},5623:function(e){"use strict";function t(e,t,o){e instanceof RegExp&&(e=n(e,o)),t instanceof RegExp&&(t=n(t,o));var i=r(e,t,o);return i&&{start:i[0],end:i[1],pre:o.slice(0,i[0]),body:o.slice(i[0]+e.length,i[1]),post:o.slice(i[1]+t.length)}}function n(e,t){var n=t.match(e);return n?n[0]:null}function r(e,t,n){var r,o,i,a,s,l=n.indexOf(e),c=n.indexOf(t,l+1),u=l;if(l>=0&&c>0){if(e===t)return[l,c];for(r=[],i=n.length;u>=0&&!s;)u==l?(r.push(u),l=n.indexOf(e,u+1)):1==r.length?s=[r.pop(),c]:((o=r.pop())=0?l:c;r.length&&(s=[i,a])}return s}e.exports=t,t.range=r},9742:function(e,t){"use strict";t.byteLength=function(e){var t=l(e),n=t[0],r=t[1];return 3*(n+r)/4-r},t.toByteArray=function(e){var t,n,i=l(e),a=i[0],s=i[1],c=new o(function(e,t,n){return 3*(t+n)/4-n}(0,a,s)),u=0,p=s>0?a-4:a;for(n=0;n>16&255,c[u++]=t>>8&255,c[u++]=255&t;return 2===s&&(t=r[e.charCodeAt(n)]<<2|r[e.charCodeAt(n+1)]>>4,c[u++]=255&t),1===s&&(t=r[e.charCodeAt(n)]<<10|r[e.charCodeAt(n+1)]<<4|r[e.charCodeAt(n+2)]>>2,c[u++]=t>>8&255,c[u++]=255&t),c},t.fromByteArray=function(e){for(var t,r=e.length,o=r%3,i=[],a=16383,s=0,l=r-o;sl?l:s+a));return 1===o?(t=e[r-1],i.push(n[t>>2]+n[t<<4&63]+"==")):2===o&&(t=(e[r-2]<<8)+e[r-1],i.push(n[t>>10]+n[t>>4&63]+n[t<<2&63]+"=")),i.join("")};for(var n=[],r=[],o="undefined"!=typeof Uint8Array?Uint8Array:Array,i="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",a=0,s=i.length;a0)throw new Error("Invalid string. Length must be a multiple of 4");var n=e.indexOf("=");return-1===n&&(n=t),[n,n===t?0:4-n%4]}function c(e,t,r){for(var o,i,a=[],s=t;s>18&63]+n[i>>12&63]+n[i>>6&63]+n[63&i]);return a.join("")}r["-".charCodeAt(0)]=62,r["_".charCodeAt(0)]=63},3644:function(e,t,n){var r=n(1048),o=n(5623);e.exports=function(e){return e?("{}"===e.substr(0,2)&&(e="\\{\\}"+e.substr(2)),g(function(e){return e.split("\\\\").join(i).split("\\{").join(a).split("\\}").join(s).split("\\,").join(l).split("\\.").join(c)}(e),!0).map(p)):[]};var i="\0SLASH"+Math.random()+"\0",a="\0OPEN"+Math.random()+"\0",s="\0CLOSE"+Math.random()+"\0",l="\0COMMA"+Math.random()+"\0",c="\0PERIOD"+Math.random()+"\0";function u(e){return parseInt(e,10)==e?parseInt(e,10):e.charCodeAt(0)}function p(e){return e.split(i).join("\\").split(a).join("{").split(s).join("}").split(l).join(",").split(c).join(".")}function f(e){if(!e)return[""];var t=[],n=o("{","}",e);if(!n)return e.split(",");var r=n.pre,i=n.body,a=n.post,s=r.split(",");s[s.length-1]+="{"+i+"}";var l=f(a);return a.length&&(s[s.length-1]+=l.shift(),s.push.apply(s,l)),t.push.apply(t,s),t}function d(e){return"{"+e+"}"}function h(e){return/^-?0\d/.test(e)}function m(e,t){return e<=t}function v(e,t){return e>=t}function g(e,t){var n=[],i=o("{","}",e);if(!i||/\$$/.test(i.pre))return[e];var a,l=/^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(i.body),c=/^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(i.body),p=l||c,y=i.body.indexOf(",")>=0;if(!p&&!y)return i.post.match(/,.*\}/)?g(e=i.pre+"{"+i.body+s+i.post):[e];if(p)a=i.body.split(/\.\./);else if(1===(a=f(i.body)).length&&1===(a=g(a[0],!1).map(d)).length)return(w=i.post.length?g(i.post,!1):[""]).map((function(e){return i.pre+a[0]+e}));var b,x=i.pre,w=i.post.length?g(i.post,!1):[""];if(p){var k=u(a[0]),E=u(a[1]),S=Math.max(a[0].length,a[1].length),_=3==a.length?Math.abs(u(a[2])):1,O=m;E0){var R=new Array(T+1).join("0");C=I<0?"-"+R+C.slice(1):R+C}}b.push(C)}}else b=r(a,(function(e){return g(e,!1)}));for(var P=0;Pa)throw new RangeError('The value "'+e+'" is invalid for option "size"');var t=new Uint8Array(e);return Object.setPrototypeOf(t,l.prototype),t}function l(e,t,n){if("number"==typeof e){if("string"==typeof t)throw new TypeError('The "string" argument must be of type string. Received type number');return p(e)}return c(e,t,n)}function c(e,t,n){if("string"==typeof e)return function(e,t){if("string"==typeof t&&""!==t||(t="utf8"),!l.isEncoding(t))throw new TypeError("Unknown encoding: "+t);var n=0|m(e,t),r=s(n),o=r.write(e,t);return o!==n&&(r=r.slice(0,o)),r}(e,t);if(ArrayBuffer.isView(e))return function(e){if(B(e,Uint8Array)){var t=new Uint8Array(e);return d(t.buffer,t.byteOffset,t.byteLength)}return f(e)}(e);if(null==e)throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof e);if(B(e,ArrayBuffer)||e&&B(e.buffer,ArrayBuffer))return d(e,t,n);if("undefined"!=typeof SharedArrayBuffer&&(B(e,SharedArrayBuffer)||e&&B(e.buffer,SharedArrayBuffer)))return d(e,t,n);if("number"==typeof e)throw new TypeError('The "value" argument must not be of type number. Received type number');var r=e.valueOf&&e.valueOf();if(null!=r&&r!==e)return l.from(r,t,n);var o=function(e){if(l.isBuffer(e)){var t=0|h(e.length),n=s(t);return 0===n.length||e.copy(n,0,0,t),n}return void 0!==e.length?"number"!=typeof e.length||$(e.length)?s(0):f(e):"Buffer"===e.type&&Array.isArray(e.data)?f(e.data):void 0}(e);if(o)return o;if("undefined"!=typeof Symbol&&null!=Symbol.toPrimitive&&"function"==typeof e[Symbol.toPrimitive])return l.from(e[Symbol.toPrimitive]("string"),t,n);throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof e)}function u(e){if("number"!=typeof e)throw new TypeError('"size" argument must be of type number');if(e<0)throw new RangeError('The value "'+e+'" is invalid for option "size"')}function p(e){return u(e),s(e<0?0:0|h(e))}function f(e){for(var t=e.length<0?0:0|h(e.length),n=s(t),r=0;r=a)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+a.toString(16)+" bytes");return 0|e}function m(e,t){if(l.isBuffer(e))return e.length;if(ArrayBuffer.isView(e)||B(e,ArrayBuffer))return e.byteLength;if("string"!=typeof e)throw new TypeError('The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type '+typeof e);var n=e.length,r=arguments.length>2&&!0===arguments[2];if(!r&&0===n)return 0;for(var o=!1;;)switch(t){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":return F(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return z(e).length;default:if(o)return r?-1:F(e).length;t=(""+t).toLowerCase(),o=!0}}function v(e,t,n){var r=!1;if((void 0===t||t<0)&&(t=0),t>this.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if((n>>>=0)<=(t>>>=0))return"";for(e||(e="utf8");;)switch(e){case"hex":return T(this,t,n);case"utf8":case"utf-8":return O(this,t,n);case"ascii":return I(this,t,n);case"latin1":case"binary":return C(this,t,n);case"base64":return _(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return R(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0}}function g(e,t,n){var r=e[t];e[t]=e[n],e[n]=r}function y(e,t,n,r,o){if(0===e.length)return-1;if("string"==typeof n?(r=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),$(n=+n)&&(n=o?0:e.length-1),n<0&&(n=e.length+n),n>=e.length){if(o)return-1;n=e.length-1}else if(n<0){if(!o)return-1;n=0}if("string"==typeof t&&(t=l.from(t,r)),l.isBuffer(t))return 0===t.length?-1:b(e,t,n,r,o);if("number"==typeof t)return t&=255,"function"==typeof Uint8Array.prototype.indexOf?o?Uint8Array.prototype.indexOf.call(e,t,n):Uint8Array.prototype.lastIndexOf.call(e,t,n):b(e,[t],n,r,o);throw new TypeError("val must be string, number or Buffer")}function b(e,t,n,r,o){var i,a=1,s=e.length,l=t.length;if(void 0!==r&&("ucs2"===(r=String(r).toLowerCase())||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(e.length<2||t.length<2)return-1;a=2,s/=2,l/=2,n/=2}function c(e,t){return 1===a?e[t]:e.readUInt16BE(t*a)}if(o){var u=-1;for(i=n;is&&(n=s-l),i=n;i>=0;i--){for(var p=!0,f=0;fo&&(r=o):r=o;var i=t.length;r>i/2&&(r=i/2);for(var a=0;a>8,o=n%256,i.push(o),i.push(r);return i}(t,e.length-n),e,n,r)}function _(e,t,n){return 0===t&&n===e.length?r.fromByteArray(e):r.fromByteArray(e.slice(t,n))}function O(e,t,n){n=Math.min(e.length,n);for(var r=[],o=t;o239?4:c>223?3:c>191?2:1;if(o+p<=n)switch(p){case 1:c<128&&(u=c);break;case 2:128==(192&(i=e[o+1]))&&(l=(31&c)<<6|63&i)>127&&(u=l);break;case 3:i=e[o+1],a=e[o+2],128==(192&i)&&128==(192&a)&&(l=(15&c)<<12|(63&i)<<6|63&a)>2047&&(l<55296||l>57343)&&(u=l);break;case 4:i=e[o+1],a=e[o+2],s=e[o+3],128==(192&i)&&128==(192&a)&&128==(192&s)&&(l=(15&c)<<18|(63&i)<<12|(63&a)<<6|63&s)>65535&&l<1114112&&(u=l)}null===u?(u=65533,p=1):u>65535&&(u-=65536,r.push(u>>>10&1023|55296),u=56320|1023&u),r.push(u),o+=p}return function(e){var t=e.length;if(t<=A)return String.fromCharCode.apply(String,e);for(var n="",r=0;rr.length?l.from(i).copy(r,o):Uint8Array.prototype.set.call(r,i,o);else{if(!l.isBuffer(i))throw new TypeError('"list" argument must be an Array of Buffers');i.copy(r,o)}o+=i.length}return r},l.byteLength=m,l.prototype._isBuffer=!0,l.prototype.swap16=function(){var e=this.length;if(e%2!=0)throw new RangeError("Buffer size must be a multiple of 16-bits");for(var t=0;tn&&(e+=" ... "),""},i&&(l.prototype[i]=l.prototype.inspect),l.prototype.compare=function(e,t,n,r,o){if(B(e,Uint8Array)&&(e=l.from(e,e.offset,e.byteLength)),!l.isBuffer(e))throw new TypeError('The "target" argument must be one of type Buffer or Uint8Array. Received type '+typeof e);if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===o&&(o=this.length),t<0||n>e.length||r<0||o>this.length)throw new RangeError("out of range index");if(r>=o&&t>=n)return 0;if(r>=o)return-1;if(t>=n)return 1;if(this===e)return 0;for(var i=(o>>>=0)-(r>>>=0),a=(n>>>=0)-(t>>>=0),s=Math.min(i,a),c=this.slice(r,o),u=e.slice(t,n),p=0;p>>=0,isFinite(n)?(n>>>=0,void 0===r&&(r="utf8")):(r=n,n=void 0)}var o=this.length-t;if((void 0===n||n>o)&&(n=o),e.length>0&&(n<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var i=!1;;)switch(r){case"hex":return x(this,e,t,n);case"utf8":case"utf-8":return w(this,e,t,n);case"ascii":case"latin1":case"binary":return k(this,e,t,n);case"base64":return E(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return S(this,e,t,n);default:if(i)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),i=!0}},l.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var A=4096;function I(e,t,n){var r="";n=Math.min(e.length,n);for(var o=t;or)&&(n=r);for(var o="",i=t;in)throw new RangeError("Trying to access beyond buffer length")}function j(e,t,n,r,o,i){if(!l.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>o||te.length)throw new RangeError("Index out of range")}function L(e,t,n,r,o,i){if(n+r>e.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("Index out of range")}function N(e,t,n,r,i){return t=+t,n>>>=0,i||L(e,0,n,4),o.write(e,t,n,r,23,4),n+4}function M(e,t,n,r,i){return t=+t,n>>>=0,i||L(e,0,n,8),o.write(e,t,n,r,52,8),n+8}l.prototype.slice=function(e,t){var n=this.length;(e=~~e)<0?(e+=n)<0&&(e=0):e>n&&(e=n),(t=void 0===t?n:~~t)<0?(t+=n)<0&&(t=0):t>n&&(t=n),t>>=0,t>>>=0,n||P(e,t,this.length);for(var r=this[e],o=1,i=0;++i>>=0,t>>>=0,n||P(e,t,this.length);for(var r=this[e+--t],o=1;t>0&&(o*=256);)r+=this[e+--t]*o;return r},l.prototype.readUint8=l.prototype.readUInt8=function(e,t){return e>>>=0,t||P(e,1,this.length),this[e]},l.prototype.readUint16LE=l.prototype.readUInt16LE=function(e,t){return e>>>=0,t||P(e,2,this.length),this[e]|this[e+1]<<8},l.prototype.readUint16BE=l.prototype.readUInt16BE=function(e,t){return e>>>=0,t||P(e,2,this.length),this[e]<<8|this[e+1]},l.prototype.readUint32LE=l.prototype.readUInt32LE=function(e,t){return e>>>=0,t||P(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},l.prototype.readUint32BE=l.prototype.readUInt32BE=function(e,t){return e>>>=0,t||P(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},l.prototype.readIntLE=function(e,t,n){e>>>=0,t>>>=0,n||P(e,t,this.length);for(var r=this[e],o=1,i=0;++i=(o*=128)&&(r-=Math.pow(2,8*t)),r},l.prototype.readIntBE=function(e,t,n){e>>>=0,t>>>=0,n||P(e,t,this.length);for(var r=t,o=1,i=this[e+--r];r>0&&(o*=256);)i+=this[e+--r]*o;return i>=(o*=128)&&(i-=Math.pow(2,8*t)),i},l.prototype.readInt8=function(e,t){return e>>>=0,t||P(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},l.prototype.readInt16LE=function(e,t){e>>>=0,t||P(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},l.prototype.readInt16BE=function(e,t){e>>>=0,t||P(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},l.prototype.readInt32LE=function(e,t){return e>>>=0,t||P(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},l.prototype.readInt32BE=function(e,t){return e>>>=0,t||P(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},l.prototype.readFloatLE=function(e,t){return e>>>=0,t||P(e,4,this.length),o.read(this,e,!0,23,4)},l.prototype.readFloatBE=function(e,t){return e>>>=0,t||P(e,4,this.length),o.read(this,e,!1,23,4)},l.prototype.readDoubleLE=function(e,t){return e>>>=0,t||P(e,8,this.length),o.read(this,e,!0,52,8)},l.prototype.readDoubleBE=function(e,t){return e>>>=0,t||P(e,8,this.length),o.read(this,e,!1,52,8)},l.prototype.writeUintLE=l.prototype.writeUIntLE=function(e,t,n,r){e=+e,t>>>=0,n>>>=0,r||j(this,e,t,n,Math.pow(2,8*n)-1,0);var o=1,i=0;for(this[t]=255&e;++i>>=0,n>>>=0,r||j(this,e,t,n,Math.pow(2,8*n)-1,0);var o=n-1,i=1;for(this[t+o]=255&e;--o>=0&&(i*=256);)this[t+o]=e/i&255;return t+n},l.prototype.writeUint8=l.prototype.writeUInt8=function(e,t,n){return e=+e,t>>>=0,n||j(this,e,t,1,255,0),this[t]=255&e,t+1},l.prototype.writeUint16LE=l.prototype.writeUInt16LE=function(e,t,n){return e=+e,t>>>=0,n||j(this,e,t,2,65535,0),this[t]=255&e,this[t+1]=e>>>8,t+2},l.prototype.writeUint16BE=l.prototype.writeUInt16BE=function(e,t,n){return e=+e,t>>>=0,n||j(this,e,t,2,65535,0),this[t]=e>>>8,this[t+1]=255&e,t+2},l.prototype.writeUint32LE=l.prototype.writeUInt32LE=function(e,t,n){return e=+e,t>>>=0,n||j(this,e,t,4,4294967295,0),this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e,t+4},l.prototype.writeUint32BE=l.prototype.writeUInt32BE=function(e,t,n){return e=+e,t>>>=0,n||j(this,e,t,4,4294967295,0),this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e,t+4},l.prototype.writeIntLE=function(e,t,n,r){if(e=+e,t>>>=0,!r){var o=Math.pow(2,8*n-1);j(this,e,t,n,o-1,-o)}var i=0,a=1,s=0;for(this[t]=255&e;++i>0)-s&255;return t+n},l.prototype.writeIntBE=function(e,t,n,r){if(e=+e,t>>>=0,!r){var o=Math.pow(2,8*n-1);j(this,e,t,n,o-1,-o)}var i=n-1,a=1,s=0;for(this[t+i]=255&e;--i>=0&&(a*=256);)e<0&&0===s&&0!==this[t+i+1]&&(s=1),this[t+i]=(e/a>>0)-s&255;return t+n},l.prototype.writeInt8=function(e,t,n){return e=+e,t>>>=0,n||j(this,e,t,1,127,-128),e<0&&(e=255+e+1),this[t]=255&e,t+1},l.prototype.writeInt16LE=function(e,t,n){return e=+e,t>>>=0,n||j(this,e,t,2,32767,-32768),this[t]=255&e,this[t+1]=e>>>8,t+2},l.prototype.writeInt16BE=function(e,t,n){return e=+e,t>>>=0,n||j(this,e,t,2,32767,-32768),this[t]=e>>>8,this[t+1]=255&e,t+2},l.prototype.writeInt32LE=function(e,t,n){return e=+e,t>>>=0,n||j(this,e,t,4,2147483647,-2147483648),this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24,t+4},l.prototype.writeInt32BE=function(e,t,n){return e=+e,t>>>=0,n||j(this,e,t,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e,t+4},l.prototype.writeFloatLE=function(e,t,n){return N(this,e,t,!0,n)},l.prototype.writeFloatBE=function(e,t,n){return N(this,e,t,!1,n)},l.prototype.writeDoubleLE=function(e,t,n){return M(this,e,t,!0,n)},l.prototype.writeDoubleBE=function(e,t,n){return M(this,e,t,!1,n)},l.prototype.copy=function(e,t,n,r){if(!l.isBuffer(e))throw new TypeError("argument should be a Buffer");if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),r>0&&r=this.length)throw new RangeError("Index out of range");if(r<0)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-t>>=0,n=void 0===n?this.length:n>>>0,e||(e=0),"number"==typeof e)for(i=t;i55295&&n<57344){if(!o){if(n>56319){(t-=3)>-1&&i.push(239,191,189);continue}if(a+1===r){(t-=3)>-1&&i.push(239,191,189);continue}o=n;continue}if(n<56320){(t-=3)>-1&&i.push(239,191,189),o=n;continue}n=65536+(o-55296<<10|n-56320)}else o&&(t-=3)>-1&&i.push(239,191,189);if(o=null,n<128){if((t-=1)<0)break;i.push(n)}else if(n<2048){if((t-=2)<0)break;i.push(n>>6|192,63&n|128)}else if(n<65536){if((t-=3)<0)break;i.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(n<1114112))throw new Error("Invalid code point");if((t-=4)<0)break;i.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return i}function z(e){return r.toByteArray(function(e){if((e=(e=e.split("=")[0]).trim().replace(D,"")).length<2)return"";for(;e.length%4!=0;)e+="=";return e}(e))}function U(e,t,n,r){for(var o=0;o=t.length||o>=e.length);++o)t[o+n]=e[o];return o}function B(e,t){return e instanceof t||null!=e&&null!=e.constructor&&null!=e.constructor.name&&e.constructor.name===t.name}function $(e){return e!=e}var q=function(){for(var e="0123456789abcdef",t=new Array(256),n=0;n<16;++n)for(var r=16*n,o=0;o<16;++o)t[r+o]=e[n]+e[o];return t}()},4480:function(e,t,n){"use strict";var r=n.g.process&&process.nextTick||n.g.setImmediate||function(e){setTimeout(e,0)};e.exports=function(e,t){return e?void t.then((function(t){r((function(){e(null,t)}))}),(function(t){r((function(){e(t)}))})):t}},4184:function(e,t){var n;!function(){"use strict";var r={}.hasOwnProperty;function o(){for(var e=[],t=0;t1?arguments[1]:void 0)}},8457:function(e,t,n){"use strict";var r=n(9974),o=n(7908),i=n(3411),a=n(7659),s=n(7466),l=n(6135),c=n(1246);e.exports=function(e){var t,n,u,p,f,d,h=o(e),m="function"==typeof this?this:Array,v=arguments.length,g=v>1?arguments[1]:void 0,y=void 0!==g,b=c(h),x=0;if(y&&(g=r(g,v>2?arguments[2]:void 0,2)),null==b||m==Array&&a(b))for(n=new m(t=s(h.length));t>x;x++)d=y?g(h[x],x):h[x],l(n,x,d);else for(f=(p=b.call(h)).next,n=new m;!(u=f.call(p)).done;x++)d=y?i(p,g,[u.value,x],!0):u.value,l(n,x,d);return n.length=x,n}},1318:function(e,t,n){var r=n(5656),o=n(7466),i=n(1400),a=function(e){return function(t,n,a){var s,l=r(t),c=o(l.length),u=i(a,c);if(e&&n!=n){for(;c>u;)if((s=l[u++])!=s)return!0}else for(;c>u;u++)if((e||u in l)&&l[u]===n)return e||u||0;return!e&&-1}};e.exports={includes:a(!0),indexOf:a(!1)}},2092:function(e,t,n){var r=n(9974),o=n(8361),i=n(7908),a=n(7466),s=n(5417),l=[].push,c=function(e){var t=1==e,n=2==e,c=3==e,u=4==e,p=6==e,f=7==e,d=5==e||p;return function(h,m,v,g){for(var y,b,x=i(h),w=o(x),k=r(m,v,3),E=a(w.length),S=0,_=g||s,O=t?_(h,E):n||f?_(h,0):void 0;E>S;S++)if((d||S in w)&&(b=k(y=w[S],S,x),e))if(t)O[S]=b;else if(b)switch(e){case 3:return!0;case 5:return y;case 6:return S;case 2:l.call(O,y)}else switch(e){case 4:return!1;case 7:l.call(O,y)}return p?-1:c||u?u:O}};e.exports={forEach:c(0),map:c(1),filter:c(2),some:c(3),every:c(4),find:c(5),findIndex:c(6),filterOut:c(7)}},1194:function(e,t,n){var r=n(7293),o=n(5112),i=n(7392),a=o("species");e.exports=function(e){return i>=51||!r((function(){var t=[];return(t.constructor={})[a]=function(){return{foo:1}},1!==t[e](Boolean).foo}))}},2133:function(e,t,n){"use strict";var r=n(7293);e.exports=function(e,t){var n=[][e];return!!n&&r((function(){n.call(null,t||function(){throw 1},1)}))}},4362:function(e){var t=Math.floor,n=function(e,i){var a=e.length,s=t(a/2);return a<8?r(e,i):o(n(e.slice(0,s),i),n(e.slice(s),i),i)},r=function(e,t){for(var n,r,o=e.length,i=1;i0;)e[r]=e[--r];r!==i++&&(e[r]=n)}return e},o=function(e,t,n){for(var r=e.length,o=t.length,i=0,a=0,s=[];i1?arguments[1]:void 0,3);t=t?t.next:n.first;)for(r(t.value,t.key,this);t&&t.removed;)t=t.previous},has:function(e){return!!g(this,e)}}),i(u.prototype,n?{get:function(e){var t=g(this,e);return t&&t.value},set:function(e,t){return v(this,0===e?0:e,t)}}:{add:function(e){return v(this,e=0===e?0:e,e)}}),p&&r(u.prototype,"size",{get:function(){return d(this).size}}),u},setStrong:function(e,t,n){var r=t+" Iterator",o=m(t),i=m(r);c(e,t,(function(e,t){h(this,{type:r,target:e,state:o(e),kind:t,last:void 0})}),(function(){for(var e=i(this),t=e.kind,n=e.last;n&&n.removed;)n=n.previous;return e.target&&(e.last=n=n?n.next:e.state.first)?"keys"==t?{value:n.key,done:!1}:"values"==t?{value:n.value,done:!1}:{value:[n.key,n.value],done:!1}:(e.target=void 0,{value:void 0,done:!0})}),n?"entries":"values",!n,!0),u(t)}}},9320:function(e,t,n){"use strict";var r=n(2248),o=n(2423).getWeakData,i=n(9670),a=n(111),s=n(5787),l=n(612),c=n(2092),u=n(6656),p=n(9909),f=p.set,d=p.getterFor,h=c.find,m=c.findIndex,v=0,g=function(e){return e.frozen||(e.frozen=new y)},y=function(){this.entries=[]},b=function(e,t){return h(e.entries,(function(e){return e[0]===t}))};y.prototype={get:function(e){var t=b(this,e);if(t)return t[1]},has:function(e){return!!b(this,e)},set:function(e,t){var n=b(this,e);n?n[1]=t:this.entries.push([e,t])},delete:function(e){var t=m(this.entries,(function(t){return t[0]===e}));return~t&&this.entries.splice(t,1),!!~t}},e.exports={getConstructor:function(e,t,n,c){var p=e((function(e,r){s(e,p,t),f(e,{type:t,id:v++,frozen:void 0}),null!=r&&l(r,e[c],{that:e,AS_ENTRIES:n})})),h=d(t),m=function(e,t,n){var r=h(e),a=o(i(t),!0);return!0===a?g(r).set(t,n):a[r.id]=n,e};return r(p.prototype,{delete:function(e){var t=h(this);if(!a(e))return!1;var n=o(e);return!0===n?g(t).delete(e):n&&u(n,t.id)&&delete n[t.id]},has:function(e){var t=h(this);if(!a(e))return!1;var n=o(e);return!0===n?g(t).has(e):n&&u(n,t.id)}}),r(p.prototype,n?{get:function(e){var t=h(this);if(a(e)){var n=o(e);return!0===n?g(t).get(e):n?n[t.id]:void 0}},set:function(e,t){return m(this,e,t)}}:{add:function(e){return m(this,e,!0)}}),p}}},7710:function(e,t,n){"use strict";var r=n(2109),o=n(7854),i=n(4705),a=n(1320),s=n(2423),l=n(612),c=n(5787),u=n(111),p=n(7293),f=n(7072),d=n(8003),h=n(9587);e.exports=function(e,t,n){var m=-1!==e.indexOf("Map"),v=-1!==e.indexOf("Weak"),g=m?"set":"add",y=o[e],b=y&&y.prototype,x=y,w={},k=function(e){var t=b[e];a(b,e,"add"==e?function(e){return t.call(this,0===e?0:e),this}:"delete"==e?function(e){return!(v&&!u(e))&&t.call(this,0===e?0:e)}:"get"==e?function(e){return v&&!u(e)?void 0:t.call(this,0===e?0:e)}:"has"==e?function(e){return!(v&&!u(e))&&t.call(this,0===e?0:e)}:function(e,n){return t.call(this,0===e?0:e,n),this})};if(i(e,"function"!=typeof y||!(v||b.forEach&&!p((function(){(new y).entries().next()})))))x=n.getConstructor(t,e,m,g),s.REQUIRED=!0;else if(i(e,!0)){var E=new x,S=E[g](v?{}:-0,1)!=E,_=p((function(){E.has(1)})),O=f((function(e){new y(e)})),A=!v&&p((function(){for(var e=new y,t=5;t--;)e[g](t,t);return!e.has(-0)}));O||((x=t((function(t,n){c(t,x,e);var r=h(new y,t,x);return null!=n&&l(n,r[g],{that:r,AS_ENTRIES:m}),r}))).prototype=b,b.constructor=x),(_||A)&&(k("delete"),k("has"),m&&k("get")),(A||S)&&k(g),v&&b.clear&&delete b.clear}return w[e]=x,r({global:!0,forced:x!=y},w),d(x,e),v||n.setStrong(x,e,m),x}},9920:function(e,t,n){var r=n(6656),o=n(3887),i=n(1236),a=n(3070);e.exports=function(e,t){for(var n=o(t),s=a.f,l=i.f,c=0;c"+a+""}},4994:function(e,t,n){"use strict";var r=n(3383).IteratorPrototype,o=n(30),i=n(9114),a=n(8003),s=n(7497),l=function(){return this};e.exports=function(e,t,n){var c=t+" Iterator";return e.prototype=o(r,{next:i(1,n)}),a(e,c,!1,!0),s[c]=l,e}},8880:function(e,t,n){var r=n(9781),o=n(3070),i=n(9114);e.exports=r?function(e,t,n){return o.f(e,t,i(1,n))}:function(e,t,n){return e[t]=n,e}},9114:function(e){e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},6135:function(e,t,n){"use strict";var r=n(7593),o=n(3070),i=n(9114);e.exports=function(e,t,n){var a=r(t);a in e?o.f(e,a,i(0,n)):e[a]=n}},8709:function(e,t,n){"use strict";var r=n(9670),o=n(7593);e.exports=function(e){if("string"!==e&&"number"!==e&&"default"!==e)throw TypeError("Incorrect hint");return o(r(this),"number"!==e)}},654:function(e,t,n){"use strict";var r=n(2109),o=n(4994),i=n(9518),a=n(7674),s=n(8003),l=n(8880),c=n(1320),u=n(5112),p=n(1913),f=n(7497),d=n(3383),h=d.IteratorPrototype,m=d.BUGGY_SAFARI_ITERATORS,v=u("iterator"),g="keys",y="values",b="entries",x=function(){return this};e.exports=function(e,t,n,u,d,w,k){o(n,t,u);var E,S,_,O=function(e){if(e===d&&R)return R;if(!m&&e in C)return C[e];switch(e){case g:case y:case b:return function(){return new n(this,e)}}return function(){return new n(this)}},A=t+" Iterator",I=!1,C=e.prototype,T=C[v]||C["@@iterator"]||d&&C[d],R=!m&&T||O(d),P="Array"==t&&C.entries||T;if(P&&(E=i(P.call(new e)),h!==Object.prototype&&E.next&&(p||i(E)===h||(a?a(E,h):"function"!=typeof E[v]&&l(E,v,x)),s(E,A,!0,!0),p&&(f[A]=x))),d==y&&T&&T.name!==y&&(I=!0,R=function(){return T.call(this)}),p&&!k||C[v]===R||l(C,v,R),f[t]=R,d)if(S={values:O(y),keys:w?R:O(g),entries:O(b)},k)for(_ in S)(m||I||!(_ in C))&&c(C,_,S[_]);else r({target:t,proto:!0,forced:m||I},S);return S}},7235:function(e,t,n){var r=n(857),o=n(6656),i=n(6061),a=n(3070).f;e.exports=function(e){var t=r.Symbol||(r.Symbol={});o(t,e)||a(t,e,{value:i.f(e)})}},9781:function(e,t,n){var r=n(7293);e.exports=!r((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]}))},317:function(e,t,n){var r=n(7854),o=n(111),i=r.document,a=o(i)&&o(i.createElement);e.exports=function(e){return a?i.createElement(e):{}}},8324:function(e){e.exports={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}},8886:function(e,t,n){var r=n(8113).match(/firefox\/(\d+)/i);e.exports=!!r&&+r[1]},7871:function(e){e.exports="object"==typeof window},256:function(e,t,n){var r=n(8113);e.exports=/MSIE|Trident/.test(r)},6833:function(e,t,n){var r=n(8113);e.exports=/(?:iphone|ipod|ipad).*applewebkit/i.test(r)},5268:function(e,t,n){var r=n(4326),o=n(7854);e.exports="process"==r(o.process)},1036:function(e,t,n){var r=n(8113);e.exports=/web0s(?!.*chrome)/i.test(r)},8113:function(e,t,n){var r=n(5005);e.exports=r("navigator","userAgent")||""},7392:function(e,t,n){var r,o,i=n(7854),a=n(8113),s=i.process,l=s&&s.versions,c=l&&l.v8;c?o=(r=c.split("."))[0]<4?1:r[0]+r[1]:a&&(!(r=a.match(/Edge\/(\d+)/))||r[1]>=74)&&(r=a.match(/Chrome\/(\d+)/))&&(o=r[1]),e.exports=o&&+o},8008:function(e,t,n){var r=n(8113).match(/AppleWebKit\/(\d+)\./);e.exports=!!r&&+r[1]},748:function(e){e.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},2109:function(e,t,n){var r=n(7854),o=n(1236).f,i=n(8880),a=n(1320),s=n(3505),l=n(9920),c=n(4705);e.exports=function(e,t){var n,u,p,f,d,h=e.target,m=e.global,v=e.stat;if(n=m?r:v?r[h]||s(h,{}):(r[h]||{}).prototype)for(u in t){if(f=t[u],p=e.noTargetGet?(d=o(n,u))&&d.value:n[u],!c(m?u:h+(v?".":"#")+u,e.forced)&&void 0!==p){if(typeof f==typeof p)continue;l(f,p)}(e.sham||p&&p.sham)&&i(f,"sham",!0),a(n,u,f,e)}}},7293:function(e){e.exports=function(e){try{return!!e()}catch(e){return!0}}},7007:function(e,t,n){"use strict";n(4916);var r=n(1320),o=n(2261),i=n(7293),a=n(5112),s=n(8880),l=a("species"),c=RegExp.prototype,u=!i((function(){var e=/./;return e.exec=function(){var e=[];return e.groups={a:"7"},e},"7"!=="".replace(e,"$")})),p="$0"==="a".replace(/./,"$0"),f=a("replace"),d=!!/./[f]&&""===/./[f]("a","$0"),h=!i((function(){var e=/(?:)/,t=e.exec;e.exec=function(){return t.apply(this,arguments)};var n="ab".split(e);return 2!==n.length||"a"!==n[0]||"b"!==n[1]}));e.exports=function(e,t,n,f){var m=a(e),v=!i((function(){var t={};return t[m]=function(){return 7},7!=""[e](t)})),g=v&&!i((function(){var t=!1,n=/a/;return"split"===e&&((n={}).constructor={},n.constructor[l]=function(){return n},n.flags="",n[m]=/./[m]),n.exec=function(){return t=!0,null},n[m](""),!t}));if(!v||!g||"replace"===e&&(!u||!p||d)||"split"===e&&!h){var y=/./[m],b=n(m,""[e],(function(e,t,n,r,i){var a=t.exec;return a===o||a===c.exec?v&&!i?{done:!0,value:y.call(t,n,r)}:{done:!0,value:e.call(n,t,r)}:{done:!1}}),{REPLACE_KEEPS_$0:p,REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE:d}),x=b[0],w=b[1];r(String.prototype,e,x),r(c,m,2==t?function(e,t){return w.call(e,this,t)}:function(e){return w.call(e,this)})}f&&s(c[m],"sham",!0)}},6790:function(e,t,n){"use strict";var r=n(3157),o=n(7466),i=n(9974),a=function(e,t,n,s,l,c,u,p){for(var f,d=l,h=0,m=!!u&&i(u,p,3);h0&&r(f))d=a(e,t,f,o(f.length),d,c-1)-1;else{if(d>=9007199254740991)throw TypeError("Exceed the acceptable array length");e[d]=f}d++}h++}return d};e.exports=a},6677:function(e,t,n){var r=n(7293);e.exports=!r((function(){return Object.isExtensible(Object.preventExtensions({}))}))},9974:function(e,t,n){var r=n(3099);e.exports=function(e,t,n){if(r(e),void 0===t)return e;switch(n){case 0:return function(){return e.call(t)};case 1:return function(n){return e.call(t,n)};case 2:return function(n,r){return e.call(t,n,r)};case 3:return function(n,r,o){return e.call(t,n,r,o)}}return function(){return e.apply(t,arguments)}}},5005:function(e,t,n){var r=n(857),o=n(7854),i=function(e){return"function"==typeof e?e:void 0};e.exports=function(e,t){return arguments.length<2?i(r[e])||i(o[e]):r[e]&&r[e][t]||o[e]&&o[e][t]}},1246:function(e,t,n){var r=n(648),o=n(7497),i=n(5112)("iterator");e.exports=function(e){if(null!=e)return e[i]||e["@@iterator"]||o[r(e)]}},8554:function(e,t,n){var r=n(9670),o=n(1246);e.exports=function(e){var t=o(e);if("function"!=typeof t)throw TypeError(String(e)+" is not iterable");return r(t.call(e))}},647:function(e,t,n){var r=n(7908),o=Math.floor,i="".replace,a=/\$([$&'`]|\d{1,2}|<[^>]*>)/g,s=/\$([$&'`]|\d{1,2})/g;e.exports=function(e,t,n,l,c,u){var p=n+e.length,f=l.length,d=s;return void 0!==c&&(c=r(c),d=a),i.call(u,d,(function(r,i){var a;switch(i.charAt(0)){case"$":return"$";case"&":return e;case"`":return t.slice(0,n);case"'":return t.slice(p);case"<":a=c[i.slice(1,-1)];break;default:var s=+i;if(0===s)return r;if(s>f){var u=o(s/10);return 0===u?r:u<=f?void 0===l[u-1]?i.charAt(1):l[u-1]+i.charAt(1):r}a=l[s-1]}return void 0===a?"":a}))}},7854:function(e,t,n){var r=function(e){return e&&e.Math==Math&&e};e.exports=r("object"==typeof globalThis&&globalThis)||r("object"==typeof window&&window)||r("object"==typeof self&&self)||r("object"==typeof n.g&&n.g)||function(){return this}()||Function("return this")()},6656:function(e,t,n){var r=n(7908),o={}.hasOwnProperty;e.exports=Object.hasOwn||function(e,t){return o.call(r(e),t)}},3501:function(e){e.exports={}},842:function(e,t,n){var r=n(7854);e.exports=function(e,t){var n=r.console;n&&n.error&&(1===arguments.length?n.error(e):n.error(e,t))}},490:function(e,t,n){var r=n(5005);e.exports=r("document","documentElement")},4664:function(e,t,n){var r=n(9781),o=n(7293),i=n(317);e.exports=!r&&!o((function(){return 7!=Object.defineProperty(i("div"),"a",{get:function(){return 7}}).a}))},8361:function(e,t,n){var r=n(7293),o=n(4326),i="".split;e.exports=r((function(){return!Object("z").propertyIsEnumerable(0)}))?function(e){return"String"==o(e)?i.call(e,""):Object(e)}:Object},9587:function(e,t,n){var r=n(111),o=n(7674);e.exports=function(e,t,n){var i,a;return o&&"function"==typeof(i=t.constructor)&&i!==n&&r(a=i.prototype)&&a!==n.prototype&&o(e,a),e}},2788:function(e,t,n){var r=n(5465),o=Function.toString;"function"!=typeof r.inspectSource&&(r.inspectSource=function(e){return o.call(e)}),e.exports=r.inspectSource},2423:function(e,t,n){var r=n(3501),o=n(111),i=n(6656),a=n(3070).f,s=n(9711),l=n(6677),c=s("meta"),u=0,p=Object.isExtensible||function(){return!0},f=function(e){a(e,c,{value:{objectID:"O"+ ++u,weakData:{}}})},d=e.exports={REQUIRED:!1,fastKey:function(e,t){if(!o(e))return"symbol"==typeof e?e:("string"==typeof e?"S":"P")+e;if(!i(e,c)){if(!p(e))return"F";if(!t)return"E";f(e)}return e[c].objectID},getWeakData:function(e,t){if(!i(e,c)){if(!p(e))return!0;if(!t)return!1;f(e)}return e[c].weakData},onFreeze:function(e){return l&&d.REQUIRED&&p(e)&&!i(e,c)&&f(e),e}};r[c]=!0},9909:function(e,t,n){var r,o,i,a=n(8536),s=n(7854),l=n(111),c=n(8880),u=n(6656),p=n(5465),f=n(6200),d=n(3501),h="Object already initialized",m=s.WeakMap;if(a||p.state){var v=p.state||(p.state=new m),g=v.get,y=v.has,b=v.set;r=function(e,t){if(y.call(v,e))throw new TypeError(h);return t.facade=e,b.call(v,e,t),t},o=function(e){return g.call(v,e)||{}},i=function(e){return y.call(v,e)}}else{var x=f("state");d[x]=!0,r=function(e,t){if(u(e,x))throw new TypeError(h);return t.facade=e,c(e,x,t),t},o=function(e){return u(e,x)?e[x]:{}},i=function(e){return u(e,x)}}e.exports={set:r,get:o,has:i,enforce:function(e){return i(e)?o(e):r(e,{})},getterFor:function(e){return function(t){var n;if(!l(t)||(n=o(t)).type!==e)throw TypeError("Incompatible receiver, "+e+" required");return n}}}},7659:function(e,t,n){var r=n(5112),o=n(7497),i=r("iterator"),a=Array.prototype;e.exports=function(e){return void 0!==e&&(o.Array===e||a[i]===e)}},3157:function(e,t,n){var r=n(4326);e.exports=Array.isArray||function(e){return"Array"==r(e)}},4705:function(e,t,n){var r=n(7293),o=/#|\.prototype\./,i=function(e,t){var n=s[a(e)];return n==c||n!=l&&("function"==typeof t?r(t):!!t)},a=i.normalize=function(e){return String(e).replace(o,".").toLowerCase()},s=i.data={},l=i.NATIVE="N",c=i.POLYFILL="P";e.exports=i},111:function(e){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},1913:function(e){e.exports=!1},7850:function(e,t,n){var r=n(111),o=n(4326),i=n(5112)("match");e.exports=function(e){var t;return r(e)&&(void 0!==(t=e[i])?!!t:"RegExp"==o(e))}},612:function(e,t,n){var r=n(9670),o=n(7659),i=n(7466),a=n(9974),s=n(1246),l=n(9212),c=function(e,t){this.stopped=e,this.result=t};e.exports=function(e,t,n){var u,p,f,d,h,m,v,g=n&&n.that,y=!(!n||!n.AS_ENTRIES),b=!(!n||!n.IS_ITERATOR),x=!(!n||!n.INTERRUPTED),w=a(t,g,1+y+x),k=function(e){return u&&l(u),new c(!0,e)},E=function(e){return y?(r(e),x?w(e[0],e[1],k):w(e[0],e[1])):x?w(e,k):w(e)};if(b)u=e;else{if("function"!=typeof(p=s(e)))throw TypeError("Target is not iterable");if(o(p)){for(f=0,d=i(e.length);d>f;f++)if((h=E(e[f]))&&h instanceof c)return h;return new c(!1)}u=p.call(e)}for(m=u.next;!(v=m.call(u)).done;){try{h=E(v.value)}catch(e){throw l(u),e}if("object"==typeof h&&h&&h instanceof c)return h}return new c(!1)}},9212:function(e,t,n){var r=n(9670);e.exports=function(e){var t=e.return;if(void 0!==t)return r(t.call(e)).value}},3383:function(e,t,n){"use strict";var r,o,i,a=n(7293),s=n(9518),l=n(8880),c=n(6656),u=n(5112),p=n(1913),f=u("iterator"),d=!1;[].keys&&("next"in(i=[].keys())?(o=s(s(i)))!==Object.prototype&&(r=o):d=!0);var h=null==r||a((function(){var e={};return r[f].call(e)!==e}));h&&(r={}),p&&!h||c(r,f)||l(r,f,(function(){return this})),e.exports={IteratorPrototype:r,BUGGY_SAFARI_ITERATORS:d}},7497:function(e){e.exports={}},5948:function(e,t,n){var r,o,i,a,s,l,c,u,p=n(7854),f=n(1236).f,d=n(261).set,h=n(6833),m=n(1036),v=n(5268),g=p.MutationObserver||p.WebKitMutationObserver,y=p.document,b=p.process,x=p.Promise,w=f(p,"queueMicrotask"),k=w&&w.value;k||(r=function(){var e,t;for(v&&(e=b.domain)&&e.exit();o;){t=o.fn,o=o.next;try{t()}catch(e){throw o?a():i=void 0,e}}i=void 0,e&&e.enter()},h||v||m||!g||!y?x&&x.resolve?((c=x.resolve(void 0)).constructor=x,u=c.then,a=function(){u.call(c,r)}):a=v?function(){b.nextTick(r)}:function(){d.call(p,r)}:(s=!0,l=y.createTextNode(""),new g(r).observe(l,{characterData:!0}),a=function(){l.data=s=!s})),e.exports=k||function(e){var t={fn:e,next:void 0};i&&(i.next=t),o||(o=t,a()),i=t}},3366:function(e,t,n){var r=n(7854);e.exports=r.Promise},133:function(e,t,n){var r=n(7392),o=n(7293);e.exports=!!Object.getOwnPropertySymbols&&!o((function(){var e=Symbol();return!String(e)||!(Object(e)instanceof Symbol)||!Symbol.sham&&r&&r<41}))},590:function(e,t,n){var r=n(7293),o=n(5112),i=n(1913),a=o("iterator");e.exports=!r((function(){var e=new URL("b?a=1&b=2&c=3","http://a"),t=e.searchParams,n="";return e.pathname="c%20d",t.forEach((function(e,r){t.delete("b"),n+=r+e})),i&&!e.toJSON||!t.sort||"http://a/c%20d?a=1&c=3"!==e.href||"3"!==t.get("c")||"a=1"!==String(new URLSearchParams("?a=1"))||!t[a]||"a"!==new URL("https://a@b").username||"b"!==new URLSearchParams(new URLSearchParams("a=b")).get("a")||"xn--e1aybc"!==new URL("http://тест").host||"#%D0%B1"!==new URL("http://a#б").hash||"a1c3"!==n||"x"!==new URL("http://x",void 0).host}))},8536:function(e,t,n){var r=n(7854),o=n(2788),i=r.WeakMap;e.exports="function"==typeof i&&/native code/.test(o(i))},8523:function(e,t,n){"use strict";var r=n(3099),o=function(e){var t,n;this.promise=new e((function(e,r){if(void 0!==t||void 0!==n)throw TypeError("Bad Promise constructor");t=e,n=r})),this.resolve=r(t),this.reject=r(n)};e.exports.f=function(e){return new o(e)}},3929:function(e,t,n){var r=n(7850);e.exports=function(e){if(r(e))throw TypeError("The method doesn't accept regular expressions");return e}},1574:function(e,t,n){"use strict";var r=n(9781),o=n(7293),i=n(1956),a=n(5181),s=n(5296),l=n(7908),c=n(8361),u=Object.assign,p=Object.defineProperty;e.exports=!u||o((function(){if(r&&1!==u({b:1},u(p({},"a",{enumerable:!0,get:function(){p(this,"b",{value:3,enumerable:!1})}}),{b:2})).b)return!0;var e={},t={},n=Symbol(),o="abcdefghijklmnopqrst";return e[n]=7,o.split("").forEach((function(e){t[e]=e})),7!=u({},e)[n]||i(u({},t)).join("")!=o}))?function(e,t){for(var n=l(e),o=arguments.length,u=1,p=a.f,f=s.f;o>u;)for(var d,h=c(arguments[u++]),m=p?i(h).concat(p(h)):i(h),v=m.length,g=0;v>g;)d=m[g++],r&&!f.call(h,d)||(n[d]=h[d]);return n}:u},30:function(e,t,n){var r,o=n(9670),i=n(6048),a=n(748),s=n(3501),l=n(490),c=n(317),u=n(6200)("IE_PROTO"),p=function(){},f=function(e){return" + + diff --git a/backend/eurydice/common/redoc/urls.py b/backend/eurydice/common/redoc/urls.py new file mode 100644 index 0000000..117305b --- /dev/null +++ b/backend/eurydice/common/redoc/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from django.views.generic import TemplateView + +urlpatterns = [ + path("", TemplateView.as_view(template_name="index.html"), name="redoc"), +] + +__all__ = ("urlpatterns",) diff --git a/backend/eurydice/common/utils/__init__.py b/backend/eurydice/common/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/common/utils/orm.py b/backend/eurydice/common/utils/orm.py new file mode 100644 index 0000000..25ad304 --- /dev/null +++ b/backend/eurydice/common/utils/orm.py @@ -0,0 +1,117 @@ +from typing import Any +from typing import Dict +from typing import Tuple + +from django.db import models + + +class SubqueryLeftOuterJoin: + """ + The SubqueryLeftOuterJoin class is a rewrite of the Join class that can be found + in `django.db.models.sql.datastructures.Join`. + + It allows to join on a subquery instead of an existing table (which is the default + Join class behavior). + + The SQL generated by SubqueryLeftOuterJoin is in the following format: + LEFT OUTER JOIN (SELECT ...) "t2" ON ("main_table"."t2_id" = "t2"."id") + + This class does not implement advanced features such as: + - automatic alias name creation, + - relabeled cloning, + - advanced join filtering clauses. + """ + + def __init__( + self, + subquery: models.QuerySet, + subquery_alias: str, + join_field_name_in_parent: str, + join_field_in_subquery: models.F, + ) -> None: + self.subquery_sql, self.subquery_params = subquery.query.sql_with_params() + self.subquery_alias = subquery_alias + self.join_field_in_parent = join_field_name_in_parent + self.join_field_in_subquery = join_field_in_subquery.name + + self.table_alias = "subquery" + self.table_name = "subquery" + self.parent_alias = subquery.model._meta.db_table + self.join_type = "LEFT JOIN" + self.nullable = True + self.filtered_relation = None + + def as_sql(self, compiler: Any, connection: Any) -> Tuple[str, Tuple]: + """ + Generates the full SQL for this join. + + This method is essentially a stripped down version of the `as_sql` method + that can be found in `django.db.models.sql.datastructures.Join`. + """ + + format_table_name = compiler.quote_name_unless_alias + format_column_name = connection.ops.quote_name + + sql = ( + f"{self.join_type} ({self.subquery_sql}) {self.subquery_alias}" + f" ON (" + f"{format_table_name(self.parent_alias)}" + f".{format_column_name(self.join_field_in_parent)}" + f" = " + f"{format_table_name(self.table_alias)}" + f".{format_column_name(self.join_field_in_subquery)}" + f")" + ) + + return sql, self.subquery_params + + +def make_queryset_with_subquery_join( + queryset: models.QuerySet, + subquery: models.QuerySet, + on: models.Q, + select: Dict[str, str], + subquery_alias: str = "subqueryjoin", +) -> models.QuerySet: + """ + Enrich a given queryset by LEFT OUTER JOINing it with another queryset. + + The `on` argument specifies on which fields the join should be performed : + `on=Q(field_in_parent_queryset=F("field_in_subquery"))` + + The `select` dict acts as an annotation mapping: keys will be the names of + annotated attributes added to the queryset elements, and values are the fields + to select from the subquery to build those attributes. + + Args: + queryset: the parent queryset (main SQL query) + subquery: the queryset to join to the parent (SQL subquery) + on: join fields expression + select: extra fields from the subquery to select in the parent query + subquery_alias: when chaining multiple querysets, must be unique at each call + + Returns: + The parent queryset, enriched with the LEFT OUTER JOIN + + """ + + # Include selected fields in the SELECT clause + queryset = queryset.extra( # nosec + select={ + parent_field: f"{subquery_alias}.{subquery_field}" + for parent_field, subquery_field in select.items() + } + ) + + # Perform the join using the internal Django query builder + join = SubqueryLeftOuterJoin( + subquery, + subquery_alias, + *(on.deconstruct()[1][0]), + ) + + queryset.query.join(join) # type: ignore + join.table_alias = subquery_alias + queryset.query.external_aliases[subquery_alias] = True # type: ignore + + return queryset diff --git a/backend/eurydice/common/utils/s3.py b/backend/eurydice/common/utils/s3.py new file mode 100644 index 0000000..723f879 --- /dev/null +++ b/backend/eurydice/common/utils/s3.py @@ -0,0 +1,42 @@ +"""Utility functions to deal with S3 resources.""" + +import logging + +from django.conf import settings +from minio.commonconfig import ENABLED +from minio.commonconfig import Filter +from minio.error import S3Error +from minio.lifecycleconfig import Expiration +from minio.lifecycleconfig import LifecycleConfig +from minio.lifecycleconfig import Rule + +from eurydice.common import minio + +logger = logging.getLogger(__name__) + + +def create_bucket_if_does_not_exist() -> None: + """Check if configured bucket exists, create it if it does not.""" + try: + minio.client.make_bucket(bucket_name=settings.MINIO_BUCKET_NAME) + config = LifecycleConfig( + [ + Rule( + ENABLED, + rule_filter=Filter(prefix=""), + rule_id="expiration-rule", + expiration=Expiration(days=settings.MINIO_EXPIRATION_DAYS), + ) + ] + ) + minio.client.set_bucket_lifecycle(settings.MINIO_BUCKET_NAME, config) + except S3Error as error: + if error.code != "BucketAlreadyOwnedByYou": + raise error + else: + logger.info( + f"Bucket '{settings.MINIO_BUCKET_NAME}' did " f"not exist and was created" + ) + + +__all__ = ("create_bucket_if_does_not_exist",) diff --git a/backend/eurydice/common/utils/signals.py b/backend/eurydice/common/utils/signals.py new file mode 100644 index 0000000..5c30335 --- /dev/null +++ b/backend/eurydice/common/utils/signals.py @@ -0,0 +1,46 @@ +import signal +from typing import Iterable + + +class BooleanCondition: + """Object wrapping a boolean with an initial value of `initial_value`. + Upon reception of a signal referenced in `listen_to` the boolean is set to a value + opposite to its initial value. + + This class allows for the easy recording of emitted signals. + + Args: + initial_value: the initial boolean value of the object. + listen_to: the signals the object must listen and react to. + + Example: + running = BooleanCondition(initial_value=True, listen_to=(signal.SIGINT,)) + while running: + do_the_job() + + """ + + def __init__( + self, + *, + initial_value: bool = True, + listen_to: Iterable[signal.Signals] = (signal.SIGINT, signal.SIGTERM) + ) -> None: + self._initial_value = initial_value + self._value = initial_value + self._attach_handler(listen_to) + + def _attach_handler(self, listen_to: Iterable[int]) -> None: + """Register function to handle signal reception.""" + for sig in listen_to: + signal.signal(sig, self._handle_signal) + + def _handle_signal(self, *args, **kwargs) -> None: + """Method called upon signal reception.""" + self._value = not self._initial_value + + def __bool__(self) -> bool: + return self._value + + +__all__ = ("BooleanCondition",) diff --git a/backend/eurydice/destination/__init__.py b/backend/eurydice/destination/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/destination/api/__init__.py b/backend/eurydice/destination/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/destination/api/apps.py b/backend/eurydice/destination/api/apps.py new file mode 100644 index 0000000..2723076 --- /dev/null +++ b/backend/eurydice/destination/api/apps.py @@ -0,0 +1,6 @@ +from django import apps + + +class ApiConfig(apps.AppConfig): + name = "eurydice.destination.api" + label = "eurydice_destination_api" diff --git a/backend/eurydice/destination/api/docs/__init__.py b/backend/eurydice/destination/api/docs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/destination/api/docs/decorators.py b/backend/eurydice/destination/api/docs/decorators.py new file mode 100644 index 0000000..ee4c914 --- /dev/null +++ b/backend/eurydice/destination/api/docs/decorators.py @@ -0,0 +1,355 @@ +from datetime import datetime + +from django.conf import settings +from django.utils.translation import gettext_lazy as _ +from drf_spectacular import types +from drf_spectacular import utils as spectacular_utils +from rest_framework import status + +from eurydice.common.api import serializers as common_serializers +from eurydice.common.api.docs import custom_spectacular +from eurydice.common.api.docs import utils as docs +from eurydice.destination.api import exceptions +from eurydice.destination.api import serializers + +incoming_transferable_example = { + "id": "00002800-0000-1000-8000-00805f9b34fb", + "created_at": "1969-12-28T14:15:22Z", + "name": "name_on_destination_side.txt", + "sha1": "31320896aedc8d3d1aaaee156be885ba0774da73", + "size": 97, + "user_provided_meta": { + "Metadata-Folder": "/home/data/", + "Metadata-Name": "name_on_destination_side.txt", + }, + "state": "ONGOING", + "progress": 43, + "finished_at": "1969-12-28T14:16:42Z", + "expires_at": "1970-01-04T14:16:42Z", + "bytes_received": 42, +} + +incoming_transferable = spectacular_utils.extend_schema_view( + list=custom_spectacular.extend_schema( + operation_id="list-transferables", + summary=_("List incoming transferables"), + description=_((settings.DOCS_PATH / "list-transferables.md").read_text()), + parameters=[ + spectacular_utils.OpenApiParameter( + name="created_after", + description="Minimum creation date within the result set.", + type=datetime, + location=spectacular_utils.OpenApiParameter.QUERY, + ), + spectacular_utils.OpenApiParameter( + name="created_before", + description="Maximum creation date within the result set.", + type=datetime, + location=spectacular_utils.OpenApiParameter.QUERY, + ), + spectacular_utils.OpenApiParameter( + name="finished_after", + description=( + "Minimum finish date within the result set. The finish date " + "is when the transferable first became ready to download." + ), + type=datetime, + location=spectacular_utils.OpenApiParameter.QUERY, + ), + spectacular_utils.OpenApiParameter( + name="finished_before", + description=( + "Maximum finish date within the result set. The finish date " + "is when the transferable first became ready to download." + ), + type=datetime, + location=spectacular_utils.OpenApiParameter.QUERY, + ), + spectacular_utils.OpenApiParameter( + name="name", + description=( + "The name (or a part of the name, for instance '.txt') to " + "filter on." + ), + type=str, + location=spectacular_utils.OpenApiParameter.QUERY, + ), + spectacular_utils.OpenApiParameter( + name="sha1", + description="The SHA1 to filter on, hexadecimal format.", + type=str, + location=spectacular_utils.OpenApiParameter.QUERY, + ), + ], + responses={ + status.HTTP_200_OK: spectacular_utils.OpenApiResponse( + response=serializers.IncomingTransferableSerializer(many=True), + description=_("The list of transferables was successfully created."), + ), + status.HTTP_401_UNAUTHORIZED: docs.NotAuthenticatedResponse, + }, + examples=[ + spectacular_utils.OpenApiExample( + name="Transfer state", + value={ + "offset": 0, + "count": 1, + "new_items": False, + "pages": { + "previous": "ksRFmM6Mkw_DzwF=", + "current": "ksRFmM6Mkw_Dzw1=", + "next": "ksRFmM6Mkw_DzwQ=", + }, + "paginated_at": "2021-02-01 14:10:01+00:00", + "results": [incoming_transferable_example], + }, + ) + ], + code_samples=[ + { + "lang": "bash", + "label": "cURL", + "source": (settings.DOCS_PATH / "list-transferables.sh").read_text(), + } + ], + tags=[_("Transferring files")], + ), + retrieve=custom_spectacular.extend_schema( + operation_id="check-transferable", + summary=_("Check a transfer's state"), + description=_((settings.DOCS_PATH / "check-transferable.md").read_text()), + responses={ + status.HTTP_200_OK: spectacular_utils.OpenApiResponse( + response=serializers.IncomingTransferableSerializer, + description=_( + "Information about the transferable was successfully retrieved." + ), + ), + status.HTTP_401_UNAUTHORIZED: docs.NotAuthenticatedResponse, + status.HTTP_404_NOT_FOUND: docs.NotFoundResponse, + }, + examples=[ + spectacular_utils.OpenApiExample( + name="Transferable", value=incoming_transferable_example + ) + ], + code_samples=[ + { + "lang": "bash", + "label": "cURL", + "source": (settings.DOCS_PATH / "check-transferable.sh").read_text(), + } + ], + tags=[_("Transferring files")], + ), + destroy=custom_spectacular.extend_schema( + operation_id="delete-transferable", + summary=_("Delete a transferable"), + description=_((settings.DOCS_PATH / "delete-transferable.md").read_text()), + responses={ + status.HTTP_204_NO_CONTENT: spectacular_utils.OpenApiResponse( + description=_( + "The transferable was removed from Eurydice destination storage." + "There is no response body." + ), + ), + status.HTTP_401_UNAUTHORIZED: docs.NotAuthenticatedResponse, + status.HTTP_404_NOT_FOUND: docs.NotFoundResponse, + status.HTTP_409_CONFLICT: docs.create_open_api_response( + exceptions.UnsuccessfulTransferableError + ), + }, + code_samples=[ + { + "lang": "bash", + "label": "cURL", + "source": (settings.DOCS_PATH / "delete-transferable.sh").read_text(), + } + ], + tags=[_("Transferring files")], + ), + download=custom_spectacular.extend_schema( + operation_id="download-transferable", + summary=_("Retrieve a transferable"), + description=_((settings.DOCS_PATH / "download-transferable.md").read_text()), + responses={ + ( + status.HTTP_200_OK, + "application/octet-stream", + ): spectacular_utils.OpenApiResponse( + response=types.OpenApiTypes.BINARY, + description=_( + "The transferable was successfully retrieved and is returned in " + "the response body." + ), + ), + ( + status.HTTP_401_UNAUTHORIZED, + "application/json", + ): docs.NotAuthenticatedResponse, + ( + status.HTTP_403_FORBIDDEN, + "application/json", + ): docs.create_open_api_response(exceptions.TransferableErroredError), + ( + status.HTTP_404_NOT_FOUND, + "application/json", + ): docs.NotFoundResponse, + ( + status.HTTP_409_CONFLICT, + "application/json", + ): docs.create_open_api_response(exceptions.TransferableOngoingError), + ( + status.HTTP_410_GONE, + "application/json", + ): docs.create_open_api_response(exceptions.TransferableExpiredError), + # Converting the status code to a string with an extra whitespace is a hack + # to be able to have multiple responses for one status code + ( + f"{status.HTTP_410_GONE} ", + "application/json", + ): docs.create_open_api_response(exceptions.TransferableRevokedError), + }, + parameters=[ + # Hide the format query parameter, user should not be able to use it + spectacular_utils.OpenApiParameter( + name="format", + location=spectacular_utils.OpenApiParameter.QUERY, + exclude=True, + ), + spectacular_utils.OpenApiParameter( + name="Content-Length", + description="The size of the body, in bytes.", + type=str, + location=spectacular_utils.OpenApiParameter.HEADER, + response=[status.HTTP_200_OK], + ), + spectacular_utils.OpenApiParameter( + name="Digest", + description="A SHA-1 digest of the requested file.", + type=str, + location=spectacular_utils.OpenApiParameter.HEADER, + response=[status.HTTP_200_OK], + ), + spectacular_utils.OpenApiParameter( + name=f"{settings.METADATA_HEADER_PREFIX}*", + description=( + "Optional file metadata provided as HTTP headers " + "when submitting the file." + ), + type=str, + location=spectacular_utils.OpenApiParameter.HEADER, + response=[status.HTTP_200_OK], + ), + ], + code_samples=[ + { + "lang": "bash", + "label": "cURL", + "source": (settings.DOCS_PATH / "download-transferable.sh").read_text(), + } + ], + tags=[_("Transferring files")], + ), +) + +user_association = spectacular_utils.extend_schema_view( + post=custom_spectacular.extend_schema( + operation_id="associate-users", + summary=_("Link two user accounts"), + description=_((settings.DOCS_PATH / "link-user-accounts.md").read_text()), + request=common_serializers.AssociationTokenSerializer, + examples=[ + spectacular_utils.OpenApiExample( + _("Submit association token"), + description=_("The body of the request should respect this format:"), + value={ + "token": ( + "BIENAYME aggraver juteuse deferer AZOIQUE inavouee COUQUE " + "mixte chaton PERIPATE exaucant fourgue pastis ayuthia " + "FONDIS prostre HALLE TAVAUX" + ) + }, + ), + ], + responses={ + status.HTTP_204_NO_CONTENT: spectacular_utils.OpenApiResponse( + description=_("The users have been successfully associated."), + ), + status.HTTP_400_BAD_REQUEST: docs.ValidationErrorResponse, + status.HTTP_401_UNAUTHORIZED: docs.NotAuthenticatedResponse, + status.HTTP_409_CONFLICT: docs.create_open_api_response( + exceptions.AlreadyAssociatedError + ), + }, + code_samples=[ + { + "lang": "bash", + "label": "cURL", + "source": (settings.DOCS_PATH / "link-user-accounts.sh").read_text(), + } + ], + tags=[_("Account management")], + ) +) + +metrics = spectacular_utils.extend_schema_view( + get=custom_spectacular.extend_schema( + operation_id="check-rolling-metrics", + summary=_("Access rolling metrics"), + description=_((settings.DOCS_PATH / "check-rolling-metrics.md").read_text()), + responses={ + status.HTTP_200_OK: spectacular_utils.OpenApiResponse( + description=_("Rolling metrics have successfully been created."), + response=serializers.RollingMetricsSerializer, + examples=[ + spectacular_utils.OpenApiExample( + _("Read metrics"), + value={ + "ongoing_transferables": 3, + "recent_successes": 14, + "recent_errors": 0, + "last_packet_received_at": ( + "2023-09-12T16:03:57.217694+02:00" + ), + }, + ), + ], + ), + status.HTTP_401_UNAUTHORIZED: docs.NotAuthenticatedResponse, + }, + code_samples=[ + { + "lang": "bash", + "label": "cURL", + "source": (settings.DOCS_PATH / "check-rolling-metrics.sh").read_text(), + } + ], + tags=[_("Administration")], + ) +) + +receiver_status = spectacular_utils.extend_schema_view( + get=custom_spectacular.extend_schema( + operation_id="get-status", + summary=_("Get receiver status"), + responses={ + status.HTTP_200_OK: spectacular_utils.OpenApiResponse( + response=serializers.StatusSerializer, + examples=[ + spectacular_utils.OpenApiExample( + _("Get receiver status"), + value={ + "last_packet_received_at": "2023-07-24T12:39:23.320950Z", + }, + ), + ], + ), + status.HTTP_401_UNAUTHORIZED: docs.NotAuthenticatedResponse, + }, + tags=[_("Administration")], + ) +) + + +__all__ = ("incoming_transferable", "metrics", "user_association") diff --git a/backend/eurydice/destination/api/docs/static/basic-auth.md b/backend/eurydice/destination/api/docs/static/basic-auth.md new file mode 100644 index 0000000..d19d22c --- /dev/null +++ b/backend/eurydice/destination/api/docs/static/basic-auth.md @@ -0,0 +1,32 @@ +Eurydice accepts the [Basic authentication scheme](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme). + +Simply pass your credentials as a base64 encoded username/password pair through the [`Authorization`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) header. + +With curl, you may use the `-u` flag: + +```bash +curl -u $EURYDICE_USERNAME:$EURYDICE_PASSWORD "https://$EURYDICE_DESTINATION_HOST/api/v1/transferables/" +``` + +[Unsafe requests](https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP) also require you to specify the Referer (sic) header. + +Example : + +```bash +# Delete transferable with id TRANSFERABLE_ID +curl -X "DELETE" \ + "https://$EURYDICE_DESTINATION_HOST/api/v1/transferables/$TRANSFERABLE_ID/" \ + -u $EURYDICE_USERNAME:$EURYDICE_PASSWORD \ + -H "Referer: https://$EURYDICE_DESTINATION_HOST/" +``` + +For [safe requests](https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP) (notably GET requests) you don't need the Referer . Example: + +```bash +# List transferables +curl \ + "https://$EURYDICE_DESTINATION_HOST" \ + -u $EURYDICE_USERNAME:$EURYDICE_PASSWORD \ + --request-target "/api/v1/transferables/" \ + -H "Accept: application/json" +``` diff --git a/backend/eurydice/destination/api/docs/static/check-rolling-metrics.md b/backend/eurydice/destination/api/docs/static/check-rolling-metrics.md new file mode 100644 index 0000000..341daaf --- /dev/null +++ b/backend/eurydice/destination/api/docs/static/check-rolling-metrics.md @@ -0,0 +1,8 @@ +This endpoint returns information about the current state of the destination server. **Only authorized users** can access this endpoint. + +It returns two types of metrics : + +- **instant counts**: instant values at query time, for example instant transferable counts; +- **sliding window counts**: cumulative values within a specified time frame preceding the query. + +Sliding window metrics time frame can be configured through the `METRICS_SLIDING_WINDOW` environment variable. diff --git a/backend/eurydice/destination/api/docs/static/check-rolling-metrics.sh b/backend/eurydice/destination/api/docs/static/check-rolling-metrics.sh new file mode 100644 index 0000000..29c0111 --- /dev/null +++ b/backend/eurydice/destination/api/docs/static/check-rolling-metrics.sh @@ -0,0 +1,3 @@ +curl "https://$EURYDICE_DESTINATION_HOST/api/v1/metrics/" \ + -H "Accept: application/json" \ + -H "Authorization: Token $EURYDICE_DESTINATION_AUTHTOKEN" diff --git a/backend/eurydice/destination/api/docs/static/check-transferable.md b/backend/eurydice/destination/api/docs/static/check-transferable.md new file mode 100644 index 0000000..c5ab76d --- /dev/null +++ b/backend/eurydice/destination/api/docs/static/check-transferable.md @@ -0,0 +1,3 @@ +This endpoint returns information about an outgoing transferable, including its transfer state. + +You may notice that the estimated time of arrival is only displayed on the origin-side API, this is because it is calculated in real time based on information the destination-side API does not know. diff --git a/backend/eurydice/destination/api/docs/static/check-transferable.sh b/backend/eurydice/destination/api/docs/static/check-transferable.sh new file mode 100644 index 0000000..643a750 --- /dev/null +++ b/backend/eurydice/destination/api/docs/static/check-transferable.sh @@ -0,0 +1,4 @@ +# Replace {id} with your transferable's ID, without the braces +curl "https://$EURYDICE_DESTINATION_HOST/api/v1/transferables/{id}/" \ + -H "Accept: application/json" \ + -H "Authorization: Token $EURYDICE_DESTINATION_AUTHTOKEN" diff --git a/backend/eurydice/destination/api/docs/static/cookie-auth.md b/backend/eurydice/destination/api/docs/static/cookie-auth.md new file mode 100644 index 0000000..314c012 --- /dev/null +++ b/backend/eurydice/destination/api/docs/static/cookie-auth.md @@ -0,0 +1,34 @@ +This is the preferred way to authenticate. + +1. Make a GET request to api/v1/user/login/ to obtain a session cookie. How to authenticate to api/v1/user/login/ depends on your reverse proxy configuration. + +For example, if it's configured to use Kerberos, you need a valid Kerberos ticket, then you can use curl's `--negotiate -u :` option. + +```bash +# This will output eurydice_sessionid and eurydice_csrftoken which you will need for the next step. +curl -c - -L --negotiate -u : "https://$EURYDICE_DESTINATION_HOST/api/v1/user/login/" -H "Accept: application/json" +``` + +2. Send eurydice_sessionid together with eurydice_csrftoken to authenticate subsequent requests. You also need to specify the Referer (sic) header. + +Example : + +```bash +# Delete transferable with id TRANSFERABLE_ID +curl -X "DELETE" \ + "https://$EURYDICE_DESTINATION_HOST/api/v1/transferables/$TRANSFERABLE_ID/" \ + --cookie "eurydice_sessionid=$SESSION_ID; eurydice_csrftoken=$CSRF_TOKEN" \ + -H "Referer: https://$EURYDICE_DESTINATION_HOST/" \ + -H "X-CSRFToken: $CSRF_TOKEN" +``` + +For [safe requests](https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP) (notably GET requests) you don't need the Referer or the CSRF token. Example: + +```bash +# List transferables +curl \ + "https://$EURYDICE_DESTINATION_HOST" \ + --cookie "eurydice_sessionid=$SESSION_ID" \ + --request-target "/api/v1/transferables/" \ + -H "Accept: application/json" +``` diff --git a/backend/eurydice/destination/api/docs/static/cookie-auth.sh b/backend/eurydice/destination/api/docs/static/cookie-auth.sh new file mode 100644 index 0000000..ad5483c --- /dev/null +++ b/backend/eurydice/destination/api/docs/static/cookie-auth.sh @@ -0,0 +1,3 @@ +curl -c - -L --negotiate -u : \ + "https://$EURYDICE_DESTINATION_HOST/api/v1/user/login/" \ + -H "Accept: application/json" diff --git a/backend/eurydice/destination/api/docs/static/delete-transferable.md b/backend/eurydice/destination/api/docs/static/delete-transferable.md new file mode 100644 index 0000000..a713ee9 --- /dev/null +++ b/backend/eurydice/destination/api/docs/static/delete-transferable.md @@ -0,0 +1,6 @@ +Delete an incoming transferable, that is, remove a received file and its transfer information. +This is **not** done automatically after retrieving the transferable. + +**Deleting transferables after retrieving them is recommended**, especially if they are large (> 100MB): it saves up space for other users as well as for your future transfers. +Please note that Eurydice does not allow for long-term file storage. Transferred files are automatically deleted after a period of time. +Nonetheless, manual deletion is recommended to prevent congestion. diff --git a/backend/eurydice/destination/api/docs/static/delete-transferable.sh b/backend/eurydice/destination/api/docs/static/delete-transferable.sh new file mode 100644 index 0000000..97284e5 --- /dev/null +++ b/backend/eurydice/destination/api/docs/static/delete-transferable.sh @@ -0,0 +1,4 @@ +# Replace {id} with your transferable's ID, without the braces +curl -X "DELETE" \ + "https://$EURYDICE_DESTINATION_HOST/api/v1/transferables/{id}/" \ + -H "Authorization: Token $EURYDICE_DESTINATION_AUTHTOKEN" diff --git a/backend/eurydice/destination/api/docs/static/download-transferable.md b/backend/eurydice/destination/api/docs/static/download-transferable.md new file mode 100644 index 0000000..02d6b0b --- /dev/null +++ b/backend/eurydice/destination/api/docs/static/download-transferable.md @@ -0,0 +1,6 @@ +Download the file corresponding to an incoming transferable. + +The file is returned in the response body. +Its integrity is automatically checked so that it is theoretically impossible to retrieve a corrupted transferable. + +During the initial transfer to Eurydice's origin API (`POST /api/v1/transferables/`), if HTTP headers starting with `Metadata-` were provided, they will be restored and returned by this endpoint in the response's HTTP headers. diff --git a/backend/eurydice/destination/api/docs/static/download-transferable.sh b/backend/eurydice/destination/api/docs/static/download-transferable.sh new file mode 100644 index 0000000..a2d0fc0 --- /dev/null +++ b/backend/eurydice/destination/api/docs/static/download-transferable.sh @@ -0,0 +1,5 @@ +# Replace {id} with your transferable's ID, without the braces +curl "https://$EURYDICE_DESTINATION_HOST/api/v1/transferables/{id}/download/" \ + -H "Accept: application/octet-stream" \ + -H "Authorization: Token $EURYDICE_DESTINATION_AUTHTOKEN" \ + -o transferred_file.txt diff --git a/backend/eurydice/destination/api/docs/static/link-user-accounts.md b/backend/eurydice/destination/api/docs/static/link-user-accounts.md new file mode 100644 index 0000000..064f48e --- /dev/null +++ b/backend/eurydice/destination/api/docs/static/link-user-accounts.md @@ -0,0 +1,31 @@ +To transfer files using Eurydice, you first need to get a user account both on the origin and on the destination side. +To obtain them, contact your local friendly system administrators, they will create the required accounts and communicate to you the credentials. + +Once this is done, you need to associate your user account located on the origin side with the one on the destination side. As the link between the origin and the destination networks is one way, this process must be performed manually to guarantee its success. + +Proceed as follow : + +- On the origin side, generate an association token : + +```bash +curl -X 'GET' +'https://$EURYDICE_ORIGIN_HOST/api/v1/user/association/' \\ + -H 'accept: application/json' \\ + -H 'Authorization: Token $EURYDICE_ORIGIN_AUTHTOKEN' +``` + +- The response holds the association token in the form of a sequence of words, together with its expiration date: + +```json +{ + "token": "infante SOIN BARBEAU agvin MARBRES AUXERRE JABOT LECHEUR rythme mascara FUN OESTRAUX ABJURANT sobriete APANAGE BEVATRON bufflant GLOSER", + "expires_at": "2021-06-29T13:57:33Z" +} +``` + +- Take good note of the words of the token. + +- Then, on the destination-side API, submit the association token obtained from the origin API (for instance using the cURL snippet provided for this request). + +The server should reply with the 204 HTTP code, meaning that the user account on the destination side was successfully associated with the one on the origin side. +If you had already transferred files before the association, they should now appear on the destination side. diff --git a/backend/eurydice/destination/api/docs/static/link-user-accounts.sh b/backend/eurydice/destination/api/docs/static/link-user-accounts.sh new file mode 100644 index 0000000..d4701f8 --- /dev/null +++ b/backend/eurydice/destination/api/docs/static/link-user-accounts.sh @@ -0,0 +1,5 @@ +curl -X "POST" \ + "https://$EURYDICE_DESTINATION_HOST/api/v1/user/association/" \ + -H "Content-Type: application/json" \ + -H "Authorization: Token $EURYDICE_DESTINATION_AUTHTOKEN" \ + -d '{ "token": "case SENSITIVE association TOKEN here" }' diff --git a/backend/eurydice/destination/api/docs/static/list-transferables.md b/backend/eurydice/destination/api/docs/static/list-transferables.md new file mode 100644 index 0000000..45080c6 --- /dev/null +++ b/backend/eurydice/destination/api/docs/static/list-transferables.md @@ -0,0 +1,11 @@ +This endpoint lists user's incoming transferables, including both those already received and those still being received. + +Please note the estimated time of arrival is only displayed on the origin-side API. +This is because it is calculated in real time based on information the destination-side API does not know. + +To navigate through the pages, request the previous, current and next page identifiers (found in the API's response) like so : `GET /api/v1/transferables/?page={identifier of the page you want}`. + +If new transferables are received while you are browsing with page identifiers, they won't be displayed in results but the new_items indicator will be set to true. +You can then request the first page again, without the page query parameter, to see them. + +N.B. While the standard way to explore transferables is to use the previous, current and next page identifiers from responses, you can also use the more advanced syntax and jump arbitrary amounts of pages via `GET /api/v1/transferables/?delta={positive or negative amount of pages}&from={any page identifier}`. diff --git a/backend/eurydice/destination/api/docs/static/list-transferables.sh b/backend/eurydice/destination/api/docs/static/list-transferables.sh new file mode 100644 index 0000000..98cbb98 --- /dev/null +++ b/backend/eurydice/destination/api/docs/static/list-transferables.sh @@ -0,0 +1,3 @@ +curl "https://$EURYDICE_DESTINATION_HOST/api/v1/transferables/" \ + -H "Accept: application/json" \ + -H "Authorization: Token $EURYDICE_DESTINATION_AUTHTOKEN" diff --git a/backend/eurydice/destination/api/docs/static/token-auth.md b/backend/eurydice/destination/api/docs/static/token-auth.md new file mode 100644 index 0000000..983fb9e --- /dev/null +++ b/backend/eurydice/destination/api/docs/static/token-auth.md @@ -0,0 +1,15 @@ +This method of authentication is intended for non human users (scripts, software etc...) only. + +Auth tokens are typically given to service accounts, and created by administrators. If you need to access the API using an auth token, ask an administrator. + +You'll have to prefix your token in the HTTP header with the keyword `Token` like so: `Authorization: Token dcd98b7102dd2f0e8b11d0f600bfb0c093` + +Example: + +```bash +# Delete transferable with id TRANSFERABLE_ID +curl -X "DELETE" \ + "https://$EURYDICE_DESTINATION_HOST/api/v1/transferables/$TRANSFERABLE_ID/" \ + -H "Accept: application/json" \ + -H "Authorization: Token $EURYDICE_ORIGIN_AUTHTOKEN" +``` diff --git a/backend/eurydice/destination/api/docs/static/user-me.sh b/backend/eurydice/destination/api/docs/static/user-me.sh new file mode 100644 index 0000000..e88727b --- /dev/null +++ b/backend/eurydice/destination/api/docs/static/user-me.sh @@ -0,0 +1,3 @@ +curl "https://$EURYDICE_DESTINATION_HOST/api/v1/user/me/" \ + -H "Accept: application/json" \ + -H "Authorization: Token $EURYDICE_DESTINATION_AUTHTOKEN" diff --git a/backend/eurydice/destination/api/exceptions.py b/backend/eurydice/destination/api/exceptions.py new file mode 100644 index 0000000..8e4829d --- /dev/null +++ b/backend/eurydice/destination/api/exceptions.py @@ -0,0 +1,84 @@ +from django.utils.translation import gettext_lazy as _ +from rest_framework import exceptions +from rest_framework import status + + +class AlreadyAssociatedError(exceptions.APIException): + status_code = status.HTTP_409_CONFLICT + default_detail = _( + "The user is already associated with a user from the origin side." + ) + # mypy does not recognize qualname + # https://github.com/python/mypy/issues/6473 + default_code = __qualname__ + + +class TransferableErroredError(exceptions.APIException): + status_code = status.HTTP_403_FORBIDDEN + default_detail = _( + "An error was encountered while processing the Transferable" + " it is not available for download." + ) + # mypy does not recognize qualname + # https://github.com/python/mypy/issues/6473 + default_code = __qualname__ + + +class TransferableExpiredError(exceptions.APIException): + status_code = status.HTTP_410_GONE + default_detail = _( + "The transferable has expired and its data was removed to limit disk usage." + ) + # mypy does not recognize qualname + # https://github.com/python/mypy/issues/6473 + default_code = __qualname__ + + +class TransferableOngoingError(exceptions.APIException): + status_code = status.HTTP_409_CONFLICT + default_detail = _( + "The transferable is still ongoing and was not yet successfully received." + ) + # mypy does not recognize qualname + # https://github.com/python/mypy/issues/6473 + default_code = __qualname__ + + +class TransferableRemovedError(exceptions.APIException): + status_code = status.HTTP_410_GONE + default_detail = _("The data of the transferable has been removed by the user.") + # mypy does not recognize qualname + # https://github.com/python/mypy/issues/6473 + default_code = __qualname__ + + +class TransferableRevokedError(exceptions.APIException): + status_code = status.HTTP_410_GONE + default_detail = _( + "The transferable has been revoked and " + "its data was removed to limit disk usage." + ) + # mypy does not recognize qualname + # https://github.com/python/mypy/issues/6473 + default_code = __qualname__ + + +class UnsuccessfulTransferableError(exceptions.APIException): + status_code = status.HTTP_409_CONFLICT + default_detail = _( + "The transferable was not successfully received and cannot be deleted." + ) + # mypy does not recognize qualname + # https://github.com/python/mypy/issues/6473 + default_code = __qualname__ + + +__all__ = ( + "AlreadyAssociatedError", + "TransferableErroredError", + "TransferableExpiredError", + "TransferableOngoingError", + "TransferableRemovedError", + "TransferableRevokedError", + "UnsuccessfulTransferableError", +) diff --git a/backend/eurydice/destination/api/filters.py b/backend/eurydice/destination/api/filters.py new file mode 100644 index 0000000..008725c --- /dev/null +++ b/backend/eurydice/destination/api/filters.py @@ -0,0 +1,26 @@ +from django_filters import rest_framework as filters + +from eurydice.common.api import filters as common_filters +from eurydice.destination.core import models + + +class IncomingTransferableFilter(filters.FilterSet): + """ + The set of filters for selecting IncomingTransferables on the destination side. + """ + + created = filters.IsoDateTimeFromToRangeFilter(field_name="created_at") + + finished = filters.IsoDateTimeFromToRangeFilter(field_name="finished_at") + + state = filters.MultipleChoiceFilter( + field_name="state", choices=models.IncomingTransferableState.choices + ) + + name = filters.CharFilter(field_name="name", lookup_expr="icontains") + + sha1 = common_filters.SHA1Filter(field_name="sha1") + + class Meta: + model = models.IncomingTransferable + fields = () diff --git a/backend/eurydice/destination/api/negotiation.py b/backend/eurydice/destination/api/negotiation.py new file mode 100644 index 0000000..ec58b82 --- /dev/null +++ b/backend/eurydice/destination/api/negotiation.py @@ -0,0 +1,13 @@ +from rest_framework import negotiation + + +class IgnoreClientContentNegotiation(negotiation.BaseContentNegotiation): + """DRF content negotiation configuration to ignore content negotiation ;)""" + + def select_renderer( + self, request, renderers, format_suffix=None # noqa: ANN001 + ): # noqa: ANN201 + """ + Select the first renderer in the `.renderer_classes` list. + """ + return (renderers[0], renderers[0].media_type) diff --git a/backend/eurydice/destination/api/permissions.py b/backend/eurydice/destination/api/permissions.py new file mode 100644 index 0000000..ae680d3 --- /dev/null +++ b/backend/eurydice/destination/api/permissions.py @@ -0,0 +1,61 @@ +"""Custom permissions for the destination API.""" + +from django.utils.translation import gettext_lazy as _ +from rest_framework import permissions +from rest_framework import request as drf_request +from rest_framework import views + +from eurydice.destination.core import models + + +def _is_associated_user(user: models.User) -> bool: + """ + Args: + user (models.User): the user to check for association + + Returns: + Whether this user is associated + """ + return ( + models.User.objects.filter( + id=user.id, + user_profile__associated_user_profile_id__isnull=False, + ) + .only("id") + .exists() + ) + + +class IsAssociatedUser(permissions.IsAuthenticated): # type: ignore + """ + Permission to only allow actions to a user that has undergone association. + """ + + message = _( + "Your destination user account must be associated with its origin user account." + ) + + def has_permission( + self, + request: drf_request.Request, + view: views.APIView, + ) -> bool: + """ + Check whether request's user is authenticated and associated + + Args: + request (drf_request.Request): the request from the user + view (views.APIView): the view associated with the request + + Returns: + whether user is authenticated and associated + """ + + is_authenticated = super().has_permission(request, view) + is_associated = False + + if is_authenticated: + # NOTE: user is authenticated, so request.user is not None + is_associated = _is_associated_user(request.user) # type: ignore + + return is_authenticated and is_associated diff --git a/backend/eurydice/destination/api/responses.py b/backend/eurydice/destination/api/responses.py new file mode 100644 index 0000000..06ce883 --- /dev/null +++ b/backend/eurydice/destination/api/responses.py @@ -0,0 +1,71 @@ +"""Custom Django Response classes.""" + +import logging +from typing import Dict +from typing import Optional + +from django import http +from minio.error import S3Error + +from eurydice.common import exceptions +from eurydice.common import minio + +logger = logging.getLogger(__name__) + + +class ForwardedS3FileResponse(http.FileResponse): + """ + HTTP Response that streams the content of a remote S3 object + through the Django API to the client. + + Attributes: + bucket_name: Name of the S3 bucket. + object_name: Name of the object in the bucket. + filename: Name of the file served to the client. + extra_headers: Extra HTTP header to include in the response. + Can override headers provided by the S3 endpoint. + as_attachment: Whether to set the Content-Disposition header + to attachment, which asks the browser to offer the file + to the user as a download. + """ + + def __init__( + self, + *, + bucket_name: str, + object_name: str, + filename: str, + extra_headers: Optional[Dict[str, str]] = None, + as_attachment: bool = True, + ): + extra_headers = extra_headers or {} + + logger.debug("Start fetching file from MinIO.") + try: + self._s3_response = minio.client.get_object( + bucket_name=bucket_name, object_name=object_name + ) + except S3Error as e: + if e.code == "NoSuchKey": + raise exceptions.S3ObjectNotFoundError() from e + raise + logger.debug("File fetched from MinIO.") + + logger.debug("Forwarding MinIO HTTP response to user.") + super().__init__( + self._s3_response, + filename=filename, + headers={ + "Content-Length": self._s3_response.headers.get("Content-Length"), + **extra_headers, + }, + as_attachment=as_attachment, + # manually override Content-Type to prevent mime sniffing + content_type="application/octet-stream", + ) + # tell Django to call self._s3_response.release_conn after reading data. + # Django calls self._s3_response.close automatically. + self._resource_closers.append(self.file_to_stream.release_conn) # type: ignore + + +__all__ = ("ForwardedS3FileResponse",) diff --git a/backend/eurydice/destination/api/serializers.py b/backend/eurydice/destination/api/serializers.py new file mode 100644 index 0000000..b718a56 --- /dev/null +++ b/backend/eurydice/destination/api/serializers.py @@ -0,0 +1,81 @@ +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from eurydice.common.api import serializers as common_serializers +from eurydice.destination.core import models + + +class StatusSerializer(serializers.Serializer): + last_packet_received_at = serializers.DateTimeField( + help_text=_("The date the last packet was received (either data or heartbeat)") + ) + + +class RollingMetricsSerializer(serializers.Serializer): + ongoing_transferables = serializers.IntegerField( + help_text=_("The amount of transferables currently being transferred"), + min_value=0, + ) + recent_successes = serializers.IntegerField( + help_text=_( + "The amount of transferables successfully transferred within the " + "last few minutes" + ), + min_value=0, + ) + recent_errors = serializers.IntegerField( + help_text=_( + "The amount of transferables that failed to be transferred within the " + "last few minutes" + ), + min_value=0, + ) + last_packet_received_at = serializers.DateTimeField( + help_text=_("The date the last packet was received (either data or heartbeat)") + ) + + +class IncomingTransferableSerializer(serializers.ModelSerializer): + sha1 = common_serializers.BytesAsHexadecimalField( + # example value + default=b"7\xf0+\xcbK\xaa\x83\xeePr|\xfe\xc3n\xdf>\xfa\xe2S<", + allow_null=True, + help_text=_("SHA-1 digest for this transferable in hexadecimal form"), + ) + + progress = serializers.IntegerField( + help_text=_( + "The percentage of bytes for this Transferable that have been " + "received from the network diode" + ), + min_value=0, + max_value=100, + ) + + expires_at = serializers.DateTimeField( + help_text=_("The Transferable will be available until that date"), + allow_null=True, + ) + + class Meta: + model = models.IncomingTransferable + fields = ( + "id", + "created_at", + "name", + "sha1", + "size", + "user_provided_meta", + "state", + "progress", + "finished_at", + "expires_at", + "bytes_received", + ) + + +__all__ = ( + "IncomingTransferableSerializer", + "RollingMetricsSerializer", + "StatusSerializer", +) diff --git a/backend/eurydice/destination/api/urls.py b/backend/eurydice/destination/api/urls.py new file mode 100644 index 0000000..fcd97f1 --- /dev/null +++ b/backend/eurydice/destination/api/urls.py @@ -0,0 +1,24 @@ +from django.urls import path +from rest_framework import routers + +import eurydice.common.api.urls +from eurydice.destination.api import views + +router = routers.DefaultRouter() +router.register( + r"transferables", views.IncomingTransferableViewSet, basename="transferable" +) + +urlpatterns = ( + eurydice.common.api.urls.urlpatterns + + router.urls + + [ + path("metrics/", views.MetricsView.as_view(), name="metrics"), + path("status/", views.StatusView.as_view(), name="status"), + path( + "user/association/", + views.UserAssociationView.as_view(), + name="user-association", + ), + ] +) diff --git a/backend/eurydice/destination/api/views/__init__.py b/backend/eurydice/destination/api/views/__init__.py new file mode 100644 index 0000000..d850eb6 --- /dev/null +++ b/backend/eurydice/destination/api/views/__init__.py @@ -0,0 +1,11 @@ +from .incoming_transferable import IncomingTransferableViewSet +from .metrics import MetricsView +from .status import StatusView +from .user_association import UserAssociationView + +__all__ = ( + "IncomingTransferableViewSet", + "MetricsView", + "UserAssociationView", + "StatusView", +) diff --git a/backend/eurydice/destination/api/views/incoming_transferable.py b/backend/eurydice/destination/api/views/incoming_transferable.py new file mode 100644 index 0000000..81bea18 --- /dev/null +++ b/backend/eurydice/destination/api/views/incoming_transferable.py @@ -0,0 +1,173 @@ +import base64 +import logging +from typing import Union + +from django.conf import settings +from django.db.models.query import QuerySet +from django_filters import rest_framework as filters +from rest_framework import decorators +from rest_framework import mixins +from rest_framework import renderers +from rest_framework import status +from rest_framework import viewsets +from rest_framework.request import Request +from rest_framework.response import Response + +from eurydice.common import exceptions +from eurydice.common import minio +from eurydice.common.api import pagination +from eurydice.common.api import permissions +from eurydice.destination.api import exceptions as api_exceptions +from eurydice.destination.api import filters as destination_filters +from eurydice.destination.api import negotiation +from eurydice.destination.api import permissions as destination_permissions +from eurydice.destination.api import responses +from eurydice.destination.api import serializers +from eurydice.destination.api.docs import decorators as documentation +from eurydice.destination.core import models +from eurydice.destination.storage import fs + +logger = logging.getLogger(__name__) + +_TRANSFERABLE_STATE_TO_ERROR = { + models.IncomingTransferableState.ONGOING: api_exceptions.TransferableOngoingError, + models.IncomingTransferableState.EXPIRED: api_exceptions.TransferableExpiredError, + models.IncomingTransferableState.REVOKED: api_exceptions.TransferableRevokedError, + models.IncomingTransferableState.ERROR: api_exceptions.TransferableErroredError, + models.IncomingTransferableState.REMOVED: api_exceptions.TransferableRemovedError, +} + + +def _minio_response( + instance: models.IncomingTransferable, filename: str, headers: dict[str, str] +) -> Union[responses.ForwardedS3FileResponse, Response]: + try: + return responses.ForwardedS3FileResponse( + bucket_name=instance.s3_bucket_name, + object_name=instance.s3_object_name, + filename=filename, + extra_headers=headers, + ) + except exceptions.S3ObjectNotFoundError: + instance.mark_as_error() + # Manually return a 500 response instead of raising an error + # so that the DB transaction is committed. + return Response( + data="Couldn't retrieve transferable", + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + +def _fs_response( + instance: models.IncomingTransferable, filename: str, headers: dict[str, str] +) -> Union[responses.http.FileResponse, Response]: # type: ignore + try: + data = open(fs.file_path(instance), "rb") # noqa: SIM115 + resp = responses.http.FileResponse( # type: ignore + data, + filename=filename, + headers={ + "Content-Length": instance.size, + **headers, + }, + as_attachment=True, + # manually override Content-Type to prevent mime sniffing + content_type="application/octet-stream", + ) + return resp + except OSError: + instance.mark_as_error() + # Manually return a 500 response instead of raising an error + # so that the DB transaction is committed. + return Response( + data="Couldn't retrieve transferable", + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + +@documentation.incoming_transferable +class IncomingTransferableViewSet( + mixins.RetrieveModelMixin, + mixins.ListModelMixin, + mixins.DestroyModelMixin, + viewsets.GenericViewSet, +): + """Retrieve, list and delete IncomingTransferables.""" + + serializer_class = serializers.IncomingTransferableSerializer + queryset = models.IncomingTransferable.objects.all() + pagination_class = pagination.EurydiceSessionPagination + permission_classes = [ + permissions.IsTransferableOwner, + destination_permissions.IsAssociatedUser, + ] + filter_backends = (filters.DjangoFilterBackend,) + filterset_class = destination_filters.IncomingTransferableFilter + + def get_queryset(self) -> QuerySet[models.IncomingTransferable]: + """Filter queryset to only retrieve IncomingTransferables for the current + user. + """ + queryset = super().get_queryset() + return queryset.filter(user_profile__user__id=self.request.user.id).order_by( + "-created_at" + ) + + def perform_destroy(self, instance: models.IncomingTransferable) -> None: + """Remove the IncomingTransferable from the storage and from the database.""" + if instance.state != models.IncomingTransferableState.SUCCESS: + raise api_exceptions.UnsuccessfulTransferableError + + if settings.MINIO_ENABLED: + logger.debug("Remove file from MinIO.") + minio.client.remove_object( + bucket_name=instance.s3_bucket_name, object_name=instance.s3_object_name + ) + logger.debug("File successfully removed from MinIO.") + else: + logger.debug("Remove file from filesystem.") + fs.delete(instance) + logger.debug("File successfully removed from filesystem.") + + instance.mark_as_removed() + logger.debug("IncomingTransferable marked as removed in database.") + + @decorators.action( + detail=True, + url_path="download", + url_name="download", + renderer_classes=[ + # JSONRenderer is only used for error responses and not for successful ones + # since successful requests use the ForwardedS3FileResponse + renderers.JSONRenderer, + ], + content_negotiation_class=negotiation.IgnoreClientContentNegotiation, + ) + def download( + self, + request: Request, + *args, + **kwargs, + ) -> Union[ # type: ignore + responses.ForwardedS3FileResponse, responses.http.FileResponse, Response + ]: + """Download the file corresponding to an IncomingTransferable.""" + logger.debug("Request IncomingTransferableState from database.") + instance = self.get_object() + + if instance.state != models.IncomingTransferableState.SUCCESS: + raise _TRANSFERABLE_STATE_TO_ERROR[instance.state] + + filename = instance.name or str(instance.id) + headers = { + **instance.user_provided_meta, + "Digest": "SHA=" + base64.b64encode(instance.sha1).decode("utf-8"), + } + + if settings.MINIO_ENABLED: + return _minio_response(instance, filename, headers) + else: + return _fs_response(instance, filename, headers) + + +__all__ = ("IncomingTransferableViewSet",) diff --git a/backend/eurydice/destination/api/views/metrics.py b/backend/eurydice/destination/api/views/metrics.py new file mode 100644 index 0000000..7a47b9a --- /dev/null +++ b/backend/eurydice/destination/api/views/metrics.py @@ -0,0 +1,56 @@ +import datetime +from typing import Dict +from typing import Optional +from typing import Union + +from django.conf import settings +from django.db.models import Count +from django.db.models import Q +from django.utils import timezone +from rest_framework import generics + +from eurydice.common.api.permissions import CanViewMetrics +from eurydice.destination.api import serializers +from eurydice.destination.api.docs import decorators as documentation +from eurydice.destination.core import models + + +@documentation.metrics +class MetricsView(generics.RetrieveAPIView): + """Access metrics about transferables.""" + + serializer_class = serializers.RollingMetricsSerializer + permission_classes = [CanViewMetrics] + + def get_object(self) -> Dict[str, Union[int, Optional[datetime.datetime]]]: + """Returns rolling metrics for the view to display.""" + metrics = models.IncomingTransferable.objects.values("state").aggregate( + ongoing_transferables=Count( + "id", filter=Q(state=models.IncomingTransferableState.ONGOING) + ), + recent_successes=Count( + "id", + filter=Q( + finished_at__gt=timezone.now() + - datetime.timedelta(seconds=settings.METRICS_SLIDING_WINDOW), + state__in=( + models.IncomingTransferableState.SUCCESS, + models.IncomingTransferableState.REMOVED, + models.IncomingTransferableState.EXPIRED, + ), + ), + ), + recent_errors=Count( + "id", + filter=Q( + finished_at__gt=timezone.now() + - datetime.timedelta(seconds=settings.METRICS_SLIDING_WINDOW), + state=models.IncomingTransferableState.ERROR, + ), + ), + ) + metrics["last_packet_received_at"] = models.LastPacketReceivedAt.get_timestamp() + return metrics + + +__all__ = ("MetricsView",) diff --git a/backend/eurydice/destination/api/views/status.py b/backend/eurydice/destination/api/views/status.py new file mode 100644 index 0000000..62cbd28 --- /dev/null +++ b/backend/eurydice/destination/api/views/status.py @@ -0,0 +1,30 @@ +import datetime +from typing import Dict +from typing import Optional + +from rest_framework import generics + +from eurydice.destination.api import serializers +from eurydice.destination.api.docs import decorators as documentation +from eurydice.destination.core import models + + +@documentation.receiver_status +class StatusView(generics.RetrieveAPIView): + """ + Retrieve receiver status. + + All authenticated users can retrieve the status. + """ + + serializer_class = serializers.StatusSerializer + + def get_object(self) -> Dict[str, Optional[datetime.datetime]]: + """Get receiver status data.""" + last_packet_received_at = models.LastPacketReceivedAt.get_timestamp() + return { + "last_packet_received_at": last_packet_received_at, + } + + +__all__ = ("StatusView",) diff --git a/backend/eurydice/destination/api/views/user_association.py b/backend/eurydice/destination/api/views/user_association.py new file mode 100644 index 0000000..ca73e2c --- /dev/null +++ b/backend/eurydice/destination/api/views/user_association.py @@ -0,0 +1,69 @@ +from typing import cast + +from django.contrib import auth +from rest_framework import request as drf_request +from rest_framework import response as drf_response +from rest_framework import status +from rest_framework import views + +from eurydice.common import association +from eurydice.common.api import serializers +from eurydice.destination.api import exceptions +from eurydice.destination.api.docs import decorators as documentation +from eurydice.destination.core import models + + +def _user_is_associated(user: models.User) -> bool: + return ( + auth.get_user_model() + .objects.filter(id=user.id, user_profile__isnull=False) + .exists() + ) + + +def _user_profile_in_token_associated(token: association.AssociationToken) -> bool: + return models.UserProfile.objects.filter( + associated_user_profile_id=token.user_profile_id, user__isnull=False + ).exists() + + +def _perform_association( + user: models.User, token: association.AssociationToken +) -> None: + models.UserProfile.objects.update_or_create( + associated_user_profile_id=token.user_profile_id, + defaults={"user": user}, + ) + + +@documentation.user_association +class UserAssociationView(views.APIView): # noqa: D101 + def post( + self, request: drf_request.Request, *args, **kwargs + ) -> drf_response.Response: + """Submit an association token obtained from the origin API to associate a user + on the origin side with a user on this side. + + Raises: + AlreadyAssociatedError: when the current authenticated user is already + associated with a user from the origin side. + + Returns: + A DRF Response of status 204 when the association succeeded. + + """ + serializer = serializers.AssociationTokenSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + + user = cast(models.User, request.user) # the user is authenticated + + token = serializer.validated_data + + if _user_is_associated(user) or _user_profile_in_token_associated(token): + raise exceptions.AlreadyAssociatedError + + _perform_association(user=user, token=token) + return drf_response.Response(status=status.HTTP_204_NO_CONTENT) + + +__all__ = ("UserAssociationView",) diff --git a/backend/eurydice/destination/backoffice/__init__.py b/backend/eurydice/destination/backoffice/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/destination/backoffice/admin.py b/backend/eurydice/destination/backoffice/admin.py new file mode 100644 index 0000000..cd27aae --- /dev/null +++ b/backend/eurydice/destination/backoffice/admin.py @@ -0,0 +1,96 @@ +# type: ignore +from typing import Dict + +from django import http +from django.contrib import admin +from django.contrib.auth import admin as auth_admin +from django.db.models import query +from django.utils.translation import gettext_lazy as _ +from rest_framework.authtoken import models as token_models + +from eurydice.common.backoffice import admin as common_admin +from eurydice.destination.api import serializers +from eurydice.destination.core import models + +admin.site.site_title = _("Eurydice destination admin") +admin.site.site_header = _("Eurydice destination - Administration") + +admin.site.unregister(token_models.TokenProxy) +admin.site.register(token_models.TokenProxy, common_admin.TokenAdmin) + + +class UserProfileInline(admin.StackedInline): + model = models.UserProfile + can_delete = False + + +@admin.register(models.User) +class UserAdmin(auth_admin.UserAdmin): + inlines = (UserProfileInline,) + list_display = auth_admin.UserAdmin.list_display + ("last_access",) + + +class S3UploadPartInline(common_admin.BaseTabularInline): + model = models.S3UploadPart + + +def _get_help_texts(*field_names: str) -> Dict[str, str]: + fields = serializers.IncomingTransferableSerializer().fields + return {name: fields[name].help_text for name in field_names} + + +@admin.register(models.IncomingTransferable) +class IncomingTransferableAdmin(common_admin.BaseModelAdmin): + list_display = ("id", "name", "size", "state", "user", "created_at") + list_filter = ("state",) + search_fields = ("id", "name", "state", "user_profile__user__username") + + inlines = (S3UploadPartInline,) + fields = ( + "name", + "hex_sha1", + "size", + "bytes_received", + "state", + "user", + "user_provided_meta", + "s3_bucket_name", + "s3_object_name", + "s3_upload_id", + "created_at", + "finished_at", + "progress", + "expires_at", + ) + help_texts = { + "hex_sha1": models.IncomingTransferable.sha1.field.help_text, + "user": _("The username of the user owning the transferable"), + **_get_help_texts("progress", "expires_at"), + } + + def get_queryset(self, request: http.HttpRequest) -> query.QuerySet: + return super().get_queryset(request).select_related("user_profile__user") + + def hex_sha1(self, obj: models.IncomingTransferable) -> str: + return obj.sha1.hex() + + hex_sha1.short_description = _("SHA-1") + + def user(self, obj: models.IncomingTransferable) -> str: + if user := obj.user_profile.user: + return user.username + + return admin.AdminSite.empty_value_display + + user.short_description = _("User") + user.admin_order_field = "user_profile__user__username" + + def progress(self, obj: models.IncomingTransferable) -> str: + return obj.progress + + progress.short_description = _("Progress") + + def expires_at(self, obj: models.IncomingTransferable) -> str: + return obj.expires_at + + expires_at.short_description = _("Expires at") diff --git a/backend/eurydice/destination/backoffice/apps.py b/backend/eurydice/destination/backoffice/apps.py new file mode 100644 index 0000000..9ea1ae5 --- /dev/null +++ b/backend/eurydice/destination/backoffice/apps.py @@ -0,0 +1,6 @@ +from django import apps + + +class DestinationBackofficeConfig(apps.AppConfig): + name = "eurydice.destination.backoffice" + label = "eurydice_destination_backoffice" diff --git a/backend/eurydice/destination/cleaning/__init__.py b/backend/eurydice/destination/cleaning/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/destination/cleaning/dbtrimmer/__init__.py b/backend/eurydice/destination/cleaning/dbtrimmer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/destination/cleaning/dbtrimmer/__main__.py b/backend/eurydice/destination/cleaning/dbtrimmer/__main__.py new file mode 100644 index 0000000..03671a2 --- /dev/null +++ b/backend/eurydice/destination/cleaning/dbtrimmer/__main__.py @@ -0,0 +1,13 @@ +if __name__ == "__main__": + import os + + import django + + os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", "eurydice.destination.config.settings.base" + ) + django.setup() + + from eurydice.destination.cleaning.dbtrimmer import dbtrimmer + + dbtrimmer.DestinationDBTrimmer().start() diff --git a/backend/eurydice/destination/cleaning/dbtrimmer/dbtrimmer.py b/backend/eurydice/destination/cleaning/dbtrimmer/dbtrimmer.py new file mode 100644 index 0000000..83af486 --- /dev/null +++ b/backend/eurydice/destination/cleaning/dbtrimmer/dbtrimmer.py @@ -0,0 +1,95 @@ +import logging +from datetime import datetime + +from django.conf import settings +from django.utils import timezone + +from eurydice.common.cleaning import repeated_task +from eurydice.destination.core import models + +logging.config.dictConfig(settings.LOGGING) # type: ignore +logger = logging.getLogger(__name__) + +BULK_DELETION_SIZE = 65_536 + + +class DestinationDBTrimmer(repeated_task.RepeatedTask): + """Removes old IncomingTransferables from the database. + Expired IncomingTransferables that finished + `settings.DBTRIMMER_TRIM_TRANSFERABLES_AFTER` ago will be removed. + The removal frequency is defined by `settings.DBTRIMMER_RUN_EVERY` and + `settings.DBTRIMMER_POLL_EVERY`. + """ + + def __init__(self) -> None: + super().__init__(settings.DBTRIMMER_RUN_EVERY, settings.DBTRIMMER_POLL_EVERY) + + def _ready(self) -> None: + """Logs that the DestinationDBTrimmer is ready before first loop.""" + logger.info("Ready") + + def _run(self) -> None: + """Delete old transferables in a final state. + + Final states are ERROR, EXPIRED, REMOVED and REVOKED. + It is safe to remove the IncomingTransferables in the states listed above, + since transaction atomicity guaranties their associated S3 objects do not + exist anymore. + """ + logger.info("DBTrimmer is running") + + remove_finished_before = ( + timezone.now() - settings.DBTRIMMER_TRIM_TRANSFERABLES_AFTER + ) + + finished = False + while not finished: + finished = self.trim_bulk(remove_finished_before) + + logger.info("DBTrimmer finished running") + + def trim_bulk(self, remove_finished_before: datetime) -> bool: + """Delete a bulk of old transferables in a final state. + + Bulk size is specified in the BULK_DELETION_SIZE variable. Since Django + retrieves TransferableRanges to delete them before deleting + OutgoingTransferables and does not clean memory as it does, it is better to + keep this value quite low to avoid excessive memory consumption. + + This function can be called in a loop as long as it returns True, meaning + transferables were successfully deleted. + """ + + to_delete = models.IncomingTransferable.objects.filter( + state__in=[ + models.IncomingTransferableState.ERROR, + models.IncomingTransferableState.EXPIRED, + models.IncomingTransferableState.REMOVED, + models.IncomingTransferableState.REVOKED, + ], + finished_at__lt=remove_finished_before, + )[:BULK_DELETION_SIZE].values_list("id", flat=True) + + delete_count = len(to_delete) + if delete_count == 0: + return True + + logger.info(f"DBTrimmer will remove {delete_count} entries.") + + # Django will implicitly split the to_delete list into blocks + # of 100 IDs each, but this seems unavoidable : + # https://code.djangoproject.com/ticket/9519 + total_deletions, _ = models.IncomingTransferable.objects.filter( + id__in=to_delete + ).delete() + + logger.info(f"DBTrimmer successfully removed {total_deletions} entries.") + + if total_deletions != delete_count: + logger.error( + f"DBTrimmer deleted {total_deletions} entries, " + f"instead of the expected {delete_count}." + ) + return True + + return False diff --git a/backend/eurydice/destination/cleaning/s3remover/__init__.py b/backend/eurydice/destination/cleaning/s3remover/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/destination/cleaning/s3remover/__main__.py b/backend/eurydice/destination/cleaning/s3remover/__main__.py new file mode 100644 index 0000000..7198502 --- /dev/null +++ b/backend/eurydice/destination/cleaning/s3remover/__main__.py @@ -0,0 +1,13 @@ +if __name__ == "__main__": + import os + + import django + + os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", "eurydice.destination.config.settings.base" + ) + django.setup() + + from eurydice.destination.cleaning.s3remover import s3remover + + s3remover.DestinationS3Remover().start() diff --git a/backend/eurydice/destination/cleaning/s3remover/s3remover.py b/backend/eurydice/destination/cleaning/s3remover/s3remover.py new file mode 100644 index 0000000..9034951 --- /dev/null +++ b/backend/eurydice/destination/cleaning/s3remover/s3remover.py @@ -0,0 +1,75 @@ +import logging +from typing import Iterator + +from django.conf import settings +from django.db import transaction +from django.utils import timezone + +from eurydice.common import minio +from eurydice.common.cleaning.repeated_task import RepeatedTask +from eurydice.destination.core import models +from eurydice.destination.storage import fs + +logging.config.dictConfig(settings.LOGGING) # type: ignore +logger = logging.getLogger(__name__) + + +class DestinationS3Remover(RepeatedTask): + """Removes old files on the S3 storage. + Files from successful IncomingTransferables that finished + `settings.S3REMOVER_EXPIRE_TRANSFERABLES_AFTER` ago will be removed. + The removal frequency is defined by `settings.S3REMOVER_RUN_EVERY` and + `settings.S3REMOVER_POLL_EVERY`. + """ + + def __init__(self) -> None: + super().__init__(settings.S3REMOVER_RUN_EVERY, settings.S3REMOVER_POLL_EVERY) + + def _ready(self) -> None: + """Logs that the S3Remover is ready before first loop.""" + logger.info("Ready") + + def _select_transferables_to_remove(self) -> Iterator[models.IncomingTransferable]: + """List successful transferables that have expired.""" + expiration_time = timezone.now() - settings.S3REMOVER_EXPIRE_TRANSFERABLES_AFTER + qset = models.IncomingTransferable.objects.filter( + state=models.IncomingTransferableState.SUCCESS, + finished_at__lt=expiration_time, + ).union( + models.IncomingTransferable.objects.filter( + state=models.IncomingTransferableState.ONGOING, + created_at__lt=expiration_time, + ).exclude(s3_upload_parts__created_at__gte=expiration_time) + ) + return qset.iterator() + + def _remove_transferable(self, transferable: models.IncomingTransferable) -> None: + """Mark the provided transferable as EXPIRED and remove its data from the + storage. + """ + with transaction.atomic(): + if transferable.state == models.IncomingTransferableState.ONGOING: + transferable.mark_as_error() + target_status = models.IncomingTransferableState.ERROR + else: + transferable.mark_as_expired() + target_status = models.IncomingTransferableState.EXPIRED + + if settings.MINIO_ENABLED: + minio.client.remove_object( + bucket_name=transferable.s3_bucket_name, + object_name=transferable.s3_object_name, + ) + else: + fs.delete(transferable) + + logger.info( + f"The IncomingTransferable {transferable.id} has been marked as " + f"{target_status.value}, " # pytype: disable=attribute-error + f"and its data removed from the storage." + ) + + def _run(self) -> None: + """Process SUCCESS transferables to expire.""" + for transferable in self._select_transferables_to_remove(): + self._remove_transferable(transferable) diff --git a/backend/eurydice/destination/config/__init__.py b/backend/eurydice/destination/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/destination/config/settings/__init__.py b/backend/eurydice/destination/config/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/destination/config/settings/base.py b/backend/eurydice/destination/config/settings/base.py new file mode 100644 index 0000000..254c959 --- /dev/null +++ b/backend/eurydice/destination/config/settings/base.py @@ -0,0 +1,170 @@ +import datetime +import pathlib + +import environ +import humanfriendly as hf +from django.utils.translation import gettext_lazy as _ + +from eurydice.common.config.settings.base import ALLOWED_HOSTS +from eurydice.common.config.settings.base import AUTH_PASSWORD_VALIDATORS +from eurydice.common.config.settings.base import AUTHENTICATION_BACKENDS +from eurydice.common.config.settings.base import BASE_DIR +from eurydice.common.config.settings.base import COMMON_DOCS_PATH +from eurydice.common.config.settings.base import CSRF_COOKIE_NAME +from eurydice.common.config.settings.base import CSRF_COOKIE_SAMESITE +from eurydice.common.config.settings.base import CSRF_COOKIE_SECURE +from eurydice.common.config.settings.base import CSRF_TRUSTED_ORIGINS +from eurydice.common.config.settings.base import DATABASES +from eurydice.common.config.settings.base import DEBUG +from eurydice.common.config.settings.base import EURYDICE_CONTACT +from eurydice.common.config.settings.base import EURYDICE_CONTACT_FR +from eurydice.common.config.settings.base import EURYDICE_VERSION +from eurydice.common.config.settings.base import INSTALLED_APPS +from eurydice.common.config.settings.base import LANGUAGE_CODE +from eurydice.common.config.settings.base import LOGGING +from eurydice.common.config.settings.base import MAX_PAGE_SIZE +from eurydice.common.config.settings.base import METADATA_HEADER_PREFIX +from eurydice.common.config.settings.base import METRICS_SLIDING_WINDOW +from eurydice.common.config.settings.base import MIDDLEWARE +from eurydice.common.config.settings.base import MINIO_ACCESS_KEY +from eurydice.common.config.settings.base import MINIO_BUCKET_NAME +from eurydice.common.config.settings.base import MINIO_ENABLED +from eurydice.common.config.settings.base import MINIO_ENDPOINT +from eurydice.common.config.settings.base import MINIO_SECRET_KEY +from eurydice.common.config.settings.base import MINIO_SECURE +from eurydice.common.config.settings.base import PAGE_SIZE +from eurydice.common.config.settings.base import REMOTE_USER_HEADER +from eurydice.common.config.settings.base import ( + REMOTE_USER_HEADER_AUTHENTICATION_ENABLED, +) +from eurydice.common.config.settings.base import REST_FRAMEWORK +from eurydice.common.config.settings.base import SECRET_KEY +from eurydice.common.config.settings.base import SECURE_PROXY_SSL_HEADER +from eurydice.common.config.settings.base import SESSION_COOKIE_AGE +from eurydice.common.config.settings.base import SESSION_COOKIE_NAME +from eurydice.common.config.settings.base import SESSION_COOKIE_SAMESITE +from eurydice.common.config.settings.base import SESSION_COOKIE_SECURE +from eurydice.common.config.settings.base import SPECTACULAR_SETTINGS +from eurydice.common.config.settings.base import STATIC_ROOT +from eurydice.common.config.settings.base import STATIC_URL +from eurydice.common.config.settings.base import TEMPLATES +from eurydice.common.config.settings.base import TIME_ZONE +from eurydice.common.config.settings.base import TRANSFERABLE_MAX_SIZE +from eurydice.common.config.settings.base import TRANSFERABLE_STORAGE_DIR +from eurydice.common.config.settings.base import UI_BADGE_COLOR +from eurydice.common.config.settings.base import UI_BADGE_CONTENT +from eurydice.common.config.settings.base import USE_I18N +from eurydice.common.config.settings.base import USE_TZ +from eurydice.common.config.settings.base import USER_ASSOCIATION_TOKEN_SECRET_KEY + +env = environ.Env( + PACKET_RECEIVER_HOST=(str, "127.0.0.1"), + PACKET_RECEIVER_PORT=(int, 65432), + PACKET_RECEIVER_TIMEOUT=(float, 0.1), + EXPECT_PACKET_EVERY=(str, "5min"), + S3REMOVER_EXPIRE_TRANSFERABLES_AFTER=(str, "7days"), + S3REMOVER_RUN_EVERY=(str, "1min"), + S3REMOVER_POLL_EVERY=(str, "200ms"), + DBTRIMMER_TRIM_TRANSFERABLES_AFTER=(str, "14days"), + DBTRIMMER_RUN_EVERY=(str, "6h"), + DBTRIMMER_POLL_EVERY=(str, "200ms"), + RECEIVER_BUFFER_MAX_ITEMS=(int, 4), +) + +DOCS_PATH = pathlib.Path(BASE_DIR) / "destination" / "api" / "docs" / "static" + +INSTALLED_APPS += [ + "eurydice.destination.core.apps.CoreConfig", + "eurydice.destination.backoffice.apps.DestinationBackofficeConfig", + "eurydice.destination.api.apps.ApiConfig", +] + +ROOT_URLCONF = "eurydice.destination.config.urls" + +WSGI_APPLICATION = "eurydice.destination.config.wsgi.application" + +AUTH_USER_MODEL = "eurydice_destination_core.User" + +# drf-spectacular +# https://drf-spectacular.readthedocs.io/ + +SPECTACULAR_SETTINGS["TITLE"] = _("Eurydice destination API") + +# Document remote user authentication only if it is enabled +if REMOTE_USER_HEADER_AUTHENTICATION_ENABLED: + SPECTACULAR_SETTINGS["APPEND_COMPONENTS"]["securitySchemes"]["cookieAuth"] = { + "type": "apiKey", + "in": "cookie", + "name": SESSION_COOKIE_NAME, + "description": _((DOCS_PATH / "cookie-auth.md").read_text()), + } +else: + SPECTACULAR_SETTINGS["APPEND_COMPONENTS"]["securitySchemes"]["basicAuth"] = { + "type": "http", + "in": "header", + "scheme": "basic", + "description": _((DOCS_PATH / "basic-auth.md").read_text()), + } + +SPECTACULAR_SETTINGS["APPEND_COMPONENTS"]["securitySchemes"]["tokenAuth"] = { + "type": "apiKey", + "in": "header", + "name": "Authorization", + "description": _((DOCS_PATH / "token-auth.md").read_text()), +} + +# The TCP server of the packet receiver is bound to this host address. +PACKET_RECEIVER_HOST = env("PACKET_RECEIVER_HOST") + +# The TCP server of the packet receiver is bound to this port. +PACKET_RECEIVER_PORT = env("PACKET_RECEIVER_PORT") + +# The duration (in seconds) after which the packet receiver timeout while waiting for +# new packets. This interrupt is needed to process received Unix signals and log +# missed heartbeats. +PACKET_RECEIVER_TIMEOUT = env("PACKET_RECEIVER_TIMEOUT") + +# How many Transferables the receiver can store in its buffer queue before dropping +# them. If the receiver receives data faster than it can process (ie. sender is faster +# than receiver) then its queue would keep increasing in size, and eventually run out +# of memory. This limit prevents that. The receiver will log an error when it has to +# drop a Transferable because of this limit. +RECEIVER_BUFFER_MAX_ITEMS = env("RECEIVER_BUFFER_MAX_ITEMS") + +# The receiver will log an error if it does not receive a packet in this time interval. +EXPECT_PACKET_EVERY = datetime.timedelta( + seconds=hf.parse_timespan(env("EXPECT_PACKET_EVERY")) +) + +# The duration after which incoming transferable data is removed and the corresponding +# object in database is marked as EXPIRED. +S3REMOVER_EXPIRE_TRANSFERABLES_AFTER = datetime.timedelta( + seconds=hf.parse_timespan(env("S3REMOVER_EXPIRE_TRANSFERABLES_AFTER")) +) + +# How often the s3remover is run. +S3REMOVER_RUN_EVERY = datetime.timedelta( + seconds=hf.parse_timespan(env("S3REMOVER_RUN_EVERY")) +) + +# How often the s3remover polls for SIGINT. +S3REMOVER_POLL_EVERY = datetime.timedelta( + seconds=hf.parse_timespan(env("S3REMOVER_POLL_EVERY")) +) + +# The duration after which transferables are deleted from the database. +DBTRIMMER_TRIM_TRANSFERABLES_AFTER = datetime.timedelta( + seconds=hf.parse_timespan(env("DBTRIMMER_TRIM_TRANSFERABLES_AFTER")) +) + +# How often the dbtrimmer is run. +DBTRIMMER_RUN_EVERY = datetime.timedelta( + seconds=hf.parse_timespan(env("DBTRIMMER_RUN_EVERY")) +) + +# How often the dbtrimmer polls for SIGINT. +DBTRIMMER_POLL_EVERY = datetime.timedelta( + seconds=hf.parse_timespan(env("DBTRIMMER_POLL_EVERY")) +) + +MINIO_EXPIRATION_DAYS = S3REMOVER_EXPIRE_TRANSFERABLES_AFTER.days + 1 diff --git a/backend/eurydice/destination/config/settings/dev.py b/backend/eurydice/destination/config/settings/dev.py new file mode 100644 index 0000000..4720dd0 --- /dev/null +++ b/backend/eurydice/destination/config/settings/dev.py @@ -0,0 +1,4 @@ +from eurydice.destination.config.settings.base import * # isort:skip + +from eurydice.common.config.settings.dev import DEBUG +from eurydice.common.config.settings.dev import FAKER_SEED diff --git a/backend/eurydice/destination/config/settings/test.py b/backend/eurydice/destination/config/settings/test.py new file mode 100644 index 0000000..9828db8 --- /dev/null +++ b/backend/eurydice/destination/config/settings/test.py @@ -0,0 +1,3 @@ +from eurydice.destination.config.settings.base import * + +from eurydice.common.config.settings.test import * # isort:skip diff --git a/backend/eurydice/destination/config/urls.py b/backend/eurydice/destination/config/urls.py new file mode 100644 index 0000000..0646e8e --- /dev/null +++ b/backend/eurydice/destination/config/urls.py @@ -0,0 +1,16 @@ +from django.urls import include +from django.urls import path + +# use DRF error views +# https://www.django-rest-framework.org/api-guide/exceptions/#generic-error-views +from eurydice.common.api.urls import handler400 # noqa: F401 +from eurydice.common.api.urls import handler500 # noqa: F401 +from eurydice.common.backoffice import urls as backoffice_urls +from eurydice.common.redoc import urls as redoc_urls +from eurydice.destination.api import urls as api_urls + +urlpatterns = [ + path("admin/", include(backoffice_urls)), + path("api/v1/", include(api_urls)), + path("api/docs/", include(redoc_urls)), +] diff --git a/backend/eurydice/destination/config/wsgi.py b/backend/eurydice/destination/config/wsgi.py new file mode 100644 index 0000000..cdb0515 --- /dev/null +++ b/backend/eurydice/destination/config/wsgi.py @@ -0,0 +1,18 @@ +""" +WSGI config for eurydice project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", "eurydice.destination.config.settings.base" +) + +application = get_wsgi_application() diff --git a/backend/eurydice/destination/core/__init__.py b/backend/eurydice/destination/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/destination/core/apps.py b/backend/eurydice/destination/core/apps.py new file mode 100644 index 0000000..63db756 --- /dev/null +++ b/backend/eurydice/destination/core/apps.py @@ -0,0 +1,20 @@ +from django import apps +from django.conf import settings +from django.db import models +from django.db.models import functions + + +class CoreConfig(apps.AppConfig): + name = "eurydice.destination.core" + label = "eurydice_destination_core" + verbose_name = "Eurydice" + + def ready(self) -> None: + if settings.MINIO_ENABLED: + from eurydice.common.utils import s3 as s3_utils + + s3_utils.create_bucket_if_does_not_exist() + + # Register lookups + models.CharField.register_lookup(functions.Length) + models.BinaryField.register_lookup(functions.Length) diff --git a/backend/eurydice/destination/core/management/__init__.py b/backend/eurydice/destination/core/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/destination/core/management/commands/__init__.py b/backend/eurydice/destination/core/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/destination/core/management/commands/populate_db.py b/backend/eurydice/destination/core/management/commands/populate_db.py new file mode 100644 index 0000000..80688e1 --- /dev/null +++ b/backend/eurydice/destination/core/management/commands/populate_db.py @@ -0,0 +1,52 @@ +from typing import Any + +import factory +from django.core.management import base +from django.db import transaction + +from tests.destination.integration import factory as destination_factory + + +class Command(base.BaseCommand): # noqa: D101 + help = "Populate the database with data resembling production" # noqa: VNE003 + + def add_arguments(self, parser: base.CommandParser) -> None: # noqa: D102 + parser.add_argument( + "--users", + type=int, + default=50, + help="Number of users to create.", + ) + parser.add_argument( + "--incoming-transferables", + type=int, + default=300, + help="Number of IncomingTransferables to create.", + ) + parser.add_argument( + "--s3-uploaded-parts", + type=int, + default=10000, + help="Number of S3UploadedParts to create.", + ) + + def handle(self, *args: Any, **options: str) -> None: + """ + Generate and populate database with data in a single query. + """ + with transaction.atomic(): + user_profiles = destination_factory.UserProfileFactory.create_batch( + options["users"] + ) + + incoming_transferables = ( + destination_factory.IncomingTransferableFactory.create_batch( + options["incoming_transferables"], + user_profile=factory.Iterator(user_profiles), + ) + ) + + destination_factory.S3UploadPartFactory.create_batch( + options["s3_uploaded_parts"], + incoming_transferable=factory.Iterator(incoming_transferables), + ) diff --git a/backend/eurydice/destination/core/migrations/0001_initial.py b/backend/eurydice/destination/core/migrations/0001_initial.py new file mode 100644 index 0000000..a97ffd6 --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0001_initial.py @@ -0,0 +1,320 @@ +# Generated by Django 3.2 on 2021-05-05 14:14 + +import uuid + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.core.validators +import django.db.models.deletion +import django.db.models.expressions +import django.utils.timezone +from django.conf import settings +from django.db import migrations +from django.db import models + +import eurydice.common.models.fields + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "abstract": False, + }, + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name="UserProfile", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "associated_user_profile_id", + models.UUIDField( + help_text="The UUID of the user profile on the origin side that is associated with the user profile on this side", + null=True, + unique=True, + verbose_name="Associated user profile ID", + ), + ), + ( + "user", + models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "db_table": "eurydice_user_profiles", + }, + ), + migrations.CreateModel( + name="IncomingTransferable", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "name", + eurydice.common.models.fields.TransferableNameField( + help_text="The name of the file corresponding to the Transferable", + max_length=255, + validators=[django.core.validators.MinLengthValidator(1)], + verbose_name="Name", + ), + ), + ( + "sha1", + eurydice.common.models.fields.SHA1Field( + help_text="The SHA-1 digest of the file corresponding to the Transferable", + max_length=20, + validators=[django.core.validators.MinLengthValidator(20)], + verbose_name="SHA-1", + ), + ), + ( + "size", + eurydice.common.models.fields.TransferableSizeField( + help_text="The size in bytes of the file corresponding to the Transferable", + validators=[ + django.core.validators.MaxValueValidator(5497558138880) + ], + verbose_name="Size in bytes", + ), + ), + ( + "s3_bucket_name", + eurydice.common.models.fields.S3BucketNameField( + help_text="The name of the S3 bucket containing the file corresponding to this IncomingTransferable", + max_length=63, + validators=[django.core.validators.MinLengthValidator(3)], + verbose_name="S3 bucket name", + ), + ), + ( + "s3_object_name", + eurydice.common.models.fields.S3ObjectNameField( + help_text="The name of the S3 object holding the data corresponding to this IncomingTransferable", + max_length=255, + validators=[django.core.validators.MinLengthValidator(1)], + verbose_name="S3 object name", + ), + ), + ( + "user_provided_meta", + eurydice.common.models.fields.UserProvidedMetaField( + default=dict, + help_text="The metadata provided by the user on file submission", + verbose_name="User provided metadata", + ), + ), + ( + "state", + models.CharField( + choices=[ + ("ONGOING", "Ongoing"), + ("SUCCESS", "Success"), + ("ERROR", "Error"), + ("EXPIRED", "Expired"), + ], + default="ONGOING", + help_text="The state of the IncomingTransferable", + max_length=7, + verbose_name="State", + ), + ), + ( + "finished_at", + models.DateTimeField( + help_text="A timestamp indicating the end of the reception of the IncomingTransferable", + null=True, + verbose_name="Transfer finish date", + ), + ), + ( + "user_profile", + models.ForeignKey( + help_text="The profile of the user owning the Transferable", + on_delete=django.db.models.deletion.RESTRICT, + to="eurydice_destination_core.userprofile", + verbose_name="User profile", + ), + ), + ], + options={ + "db_table": "eurydice_incoming_transferables", + }, + ), + migrations.AddConstraint( + model_name="userprofile", + constraint=models.CheckConstraint( + check=models.Q( + ("associated_user_profile_id__isnull", True), + ("user__isnull", True), + _negated=True, + ), + name="eurydice_destination_core_userprofile_user_associated_user_profile_id", + ), + ), + migrations.AddConstraint( + model_name="incomingtransferable", + constraint=models.UniqueConstraint( + fields=("s3_bucket_name", "s3_object_name"), + name="eurydice_destination_core_incomingtransferable_s3_bucket_name_s3_object_name", + ), + ), + migrations.AddConstraint( + model_name="incomingtransferable", + constraint=models.CheckConstraint( + check=models.Q( + models.Q(("finished_at__isnull", True), ("state", "ONGOING")), + models.Q( + ("finished_at__isnull", False), + ("state__in", ("SUCCESS", "ERROR", "EXPIRED")), + ), + _connector="OR", + ), + name="eurydice_destination_core_incomingtransferable_finished_at_state", + ), + ), + migrations.AddConstraint( + model_name="incomingtransferable", + constraint=models.CheckConstraint( + check=models.Q( + finished_at__gte=django.db.models.expressions.F("created_at") + ), + name="eurydice_destination_core_incomingtransferable_finished_at_created_at", + ), + ), + ] diff --git a/backend/eurydice/destination/core/migrations/0002_alter_user_profile_constraints.py b/backend/eurydice/destination/core/migrations/0002_alter_user_profile_constraints.py new file mode 100644 index 0000000..6865f86 --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0002_alter_user_profile_constraints.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.2 on 2021-05-19 15:14 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("eurydice_destination_core", "0001_initial"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="userprofile", + name="eurydice_destination_core_userprofile_user_associated_user_profile_id", + ), + migrations.AlterField( + model_name="userprofile", + name="associated_user_profile_id", + field=models.UUIDField( + help_text="The UUID of the user profile on the origin side that is associated with the user profile on this side", + unique=True, + verbose_name="Associated user profile ID", + ), + ), + migrations.AlterField( + model_name="userprofile", + name="user", + field=models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="user_profile", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/backend/eurydice/destination/core/migrations/0003_add_incoming_transferable_fields.py b/backend/eurydice/destination/core/migrations/0003_add_incoming_transferable_fields.py new file mode 100644 index 0000000..eba3967 --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0003_add_incoming_transferable_fields.py @@ -0,0 +1,49 @@ +# Generated by Django 3.2.3 on 2021-06-02 09:34 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("eurydice_destination_core", "0002_alter_user_profile_constraints"), + ] + + operations = [ + migrations.AddField( + model_name="incomingtransferable", + name="s3_nb_uploaded_parts", + field=models.PositiveIntegerField( + default=0, + help_text="The number of parts uploaded to S3 during the multipart upload identified with s3_upload_id", + verbose_name="The number of parts uploaded to S3", + ), + ), + migrations.AddField( + model_name="incomingtransferable", + name="s3_upload_id", + field=models.CharField( + default="", + help_text="The ID for the S3 multipart upload of this IncomingTransferable.", + max_length=89, + verbose_name="S3 upload ID", + ), + ), + migrations.AddConstraint( + model_name="incomingtransferable", + constraint=models.CheckConstraint( + check=models.Q( + models.Q( + ("s3_upload_id", ""), + ("state__in", ("SUCCESS", "ERROR", "EXPIRED")), + ), + models.Q( + ("state", "ONGOING"), + models.Q(("s3_upload_id", ""), _negated=True), + ), + _connector="OR", + ), + name="eurydice_destination_core_incomingtransferable_state_s3_upload_id", + ), + ), + ] diff --git a/backend/eurydice/destination/core/migrations/0004_update_incoming_transferable_constraints.py b/backend/eurydice/destination/core/migrations/0004_update_incoming_transferable_constraints.py new file mode 100644 index 0000000..1045ce2 --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0004_update_incoming_transferable_constraints.py @@ -0,0 +1,70 @@ +# Generated by Django 3.2.3 on 2021-06-02 14:25 + +import django.core.validators +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("eurydice_destination_core", "0003_add_incoming_transferable_fields"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="incomingtransferable", + name="eurydice_destination_core_incomingtransferable_finished_at_state", + ), + migrations.RemoveConstraint( + model_name="incomingtransferable", + name="eurydice_destination_core_incomingtransferable_state_s3_upload_id", + ), + migrations.AlterField( + model_name="incomingtransferable", + name="s3_upload_id", + field=models.CharField( + help_text="The ID for the S3 multipart upload of this IncomingTransferable.", + max_length=89, + validators=[django.core.validators.MinLengthValidator(89)], + verbose_name="S3 upload ID", + ), + ), + migrations.AlterField( + model_name="incomingtransferable", + name="state", + field=models.CharField( + choices=[ + ("ONGOING", "Ongoing"), + ("SUCCESS", "Success"), + ("ERROR", "Error"), + ("REVOKED", "Revoked"), + ("EXPIRED", "Expired"), + ], + default="ONGOING", + help_text="The state of the IncomingTransferable", + max_length=7, + verbose_name="State", + ), + ), + migrations.AddConstraint( + model_name="incomingtransferable", + constraint=models.CheckConstraint( + check=models.Q( + models.Q(("finished_at__isnull", True), ("state", "ONGOING")), + models.Q( + ("finished_at__isnull", False), + ("state__in", ("SUCCESS", "ERROR", "REVOKED", "EXPIRED")), + ), + _connector="OR", + ), + name="eurydice_destination_core_incomingtransferable_finished_at_state", + ), + ), + migrations.AddConstraint( + model_name="incomingtransferable", + constraint=models.CheckConstraint( + check=models.Q(("s3_upload_id", ""), _negated=True), + name="eurydice_destination_core_incomingtransferable_s3_upload_id", + ), + ), + ] diff --git a/backend/eurydice/destination/core/migrations/0005_update_incoming_transferable_constraints.py b/backend/eurydice/destination/core/migrations/0005_update_incoming_transferable_constraints.py new file mode 100644 index 0000000..69e29ab --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0005_update_incoming_transferable_constraints.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.3 on 2021-06-08 09:09 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("eurydice_destination_core", "0004_update_incoming_transferable_constraints"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="incomingtransferable", + name="eurydice_destination_core_incomingtransferable_s3_bucket_name_s3_object_name", + ), + migrations.RemoveConstraint( + model_name="incomingtransferable", + name="eurydice_destination_core_incomingtransferable_s3_upload_id", + ), + migrations.AddConstraint( + model_name="incomingtransferable", + constraint=models.UniqueConstraint( + condition=models.Q( + ("s3_bucket_name", ""), ("s3_object_name", ""), _negated=True + ), + fields=("s3_bucket_name", "s3_object_name"), + name="eurydice_destination_core_incomingtransferable_s3_bucket_name_s3_object_name", + ), + ), + ] diff --git a/backend/eurydice/destination/core/migrations/0006_add_incoming_transferable_rehash_intermediary.py b/backend/eurydice/destination/core/migrations/0006_add_incoming_transferable_rehash_intermediary.py new file mode 100644 index 0000000..1b611d3 --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0006_add_incoming_transferable_rehash_intermediary.py @@ -0,0 +1,46 @@ +# Generated by Django 3.2.3 on 2021-06-03 09:45 + +import django.core.validators +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("eurydice_destination_core", "0005_update_incoming_transferable_constraints"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="incomingtransferable", + name="eurydice_destination_core_incomingtransferable_finished_at_state", + ), + migrations.AddField( + model_name="incomingtransferable", + name="rehash_intermediary", + field=models.BinaryField( + default=None, + help_text="Bytes corresponding to the internal state of the hashlib object as returned by eurydice.destination.utils.rehash.sha1_to_bytes", + validators=[ + django.core.validators.MaxLengthValidator(104), + django.core.validators.MinLengthValidator(104), + ], + verbose_name="Rehash intermediary", + ), + preserve_default=False, + ), + migrations.AddConstraint( + model_name="incomingtransferable", + constraint=models.CheckConstraint( + check=models.Q( + models.Q(("finished_at__isnull", True), ("state", "ONGOING")), + models.Q( + ("finished_at__isnull", False), + ("state__in", {"ERROR", "SUCCESS", "EXPIRED", "REVOKED"}), + ), + _connector="OR", + ), + name="eurydice_destination_core_incomingtransferable_finished_at_state", + ), + ), + ] diff --git a/backend/eurydice/destination/core/migrations/0007_add_s3_upload_part.py b/backend/eurydice/destination/core/migrations/0007_add_s3_upload_part.py new file mode 100644 index 0000000..1c9e81f --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0007_add_s3_upload_part.py @@ -0,0 +1,73 @@ +# Generated by Django 3.2.3 on 2021-06-04 13:41 + +import uuid + +import django.core.validators +import django.db.models.deletion +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "eurydice_destination_core", + "0006_add_incoming_transferable_rehash_intermediary", + ), + ] + + operations = [ + migrations.CreateModel( + name="S3UploadPart", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "etag", + models.BinaryField( + help_text="Bytes corresponding to the MD5 sum for the upload part. Note that S3 multipart upload parts' ETags should always be an MD5 sum. https://docs.aws.amazon.com/AmazonS3/latest/API/API_Object.html", + max_length=16, + validators=[django.core.validators.MinLengthValidator(16)], + verbose_name="S3 multipart upload part ETag", + ), + ), + ( + "part_number", + models.PositiveIntegerField( + help_text="The index of this part within the multipart upload.", + validators=[django.core.validators.MinValueValidator(1)], + verbose_name="S3 multipart upload part number", + ), + ), + ( + "incoming_transferable", + models.ForeignKey( + help_text="The profile of the user owning the Transferable", + on_delete=django.db.models.deletion.RESTRICT, + related_name="s3_upload_parts", + to="eurydice_destination_core.incomingtransferable", + verbose_name="Incoming Transferable", + ), + ), + ], + options={ + "db_table": "eurydice_s3_upload_part", + }, + ), + migrations.AddConstraint( + model_name="s3uploadpart", + constraint=models.UniqueConstraint( + fields=("incoming_transferable", "part_number"), + name="eurydice_destination_core_s3uploadpart_incoming_transferable_part_number", + ), + ), + ] diff --git a/backend/eurydice/destination/core/migrations/0008_remove_incomingtransferable_s3_nb_uploaded_parts.py b/backend/eurydice/destination/core/migrations/0008_remove_incomingtransferable_s3_nb_uploaded_parts.py new file mode 100644 index 0000000..54d1db5 --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0008_remove_incomingtransferable_s3_nb_uploaded_parts.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.3 on 2021-06-04 10:14 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ( + "eurydice_destination_core", + "0007_add_s3_upload_part", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="incomingtransferable", + name="s3_nb_uploaded_parts", + ), + ] diff --git a/backend/eurydice/destination/core/migrations/0009_alter_incomingtransferable_rehash_intermediary.py b/backend/eurydice/destination/core/migrations/0009_alter_incomingtransferable_rehash_intermediary.py new file mode 100644 index 0000000..fb56776 --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0009_alter_incomingtransferable_rehash_intermediary.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.3 on 2021-06-23 08:49 + +import hashlib + +import django.core.validators +from django.db import migrations +from django.db import models + +from eurydice.destination.utils import rehash + + +class Migration(migrations.Migration): + dependencies = [ + ( + "eurydice_destination_core", + "0008_remove_incomingtransferable_s3_nb_uploaded_parts", + ), + ] + + operations = [ + migrations.AlterField( + model_name="incomingtransferable", + name="rehash_intermediary", + field=models.BinaryField( + default=rehash.sha1_to_bytes(hashlib.sha1(b"")), # nosec + help_text="Bytes corresponding to the internal state of the hashlib object as returned by eurydice.destination.utils.rehash.sha1_to_bytes", + validators=[ + django.core.validators.MaxLengthValidator(104), + django.core.validators.MinLengthValidator(104), + ], + verbose_name="Rehash intermediary", + ), + ), + ] diff --git a/backend/eurydice/destination/core/migrations/0010_alter_incomingtransferable_created_at.py b/backend/eurydice/destination/core/migrations/0010_alter_incomingtransferable_created_at.py new file mode 100644 index 0000000..4a7ad83 --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0010_alter_incomingtransferable_created_at.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.4 on 2021-06-30 09:46 + +import django.utils.timezone +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "eurydice_destination_core", + "0009_alter_incomingtransferable_rehash_intermediary", + ), + ] + + operations = [ + migrations.AlterField( + model_name="incomingtransferable", + name="created_at", + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/backend/eurydice/destination/core/migrations/0011_added_bytes_received_to_transferables.py b/backend/eurydice/destination/core/migrations/0011_added_bytes_received_to_transferables.py new file mode 100644 index 0000000..64ba5aa --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0011_added_bytes_received_to_transferables.py @@ -0,0 +1,36 @@ +# Generated by Django 3.2.5 on 2021-07-30 07:19 + +import django.core.validators +from django.db import migrations + +import eurydice.common.models.fields + + +class Migration(migrations.Migration): + dependencies = [ + ("eurydice_destination_core", "0010_alter_incomingtransferable_created_at"), + ] + + operations = [ + migrations.AddField( + model_name="incomingtransferable", + name="bytes_received", + field=eurydice.common.models.fields.TransferableSizeField( + default=0, + help_text="The amount of bytes received until now for the Transferable", + validators=[django.core.validators.MaxValueValidator(5497558138880)], + verbose_name="Amount of bytes received", + ), + ), + migrations.AlterField( + model_name="incomingtransferable", + name="size", + field=eurydice.common.models.fields.TransferableSizeField( + default=None, + help_text="The size in bytes of the file corresponding to the Transferable", + null=True, + validators=[django.core.validators.MaxValueValidator(5497558138880)], + verbose_name="Size in bytes", + ), + ), + ] diff --git a/backend/eurydice/destination/core/migrations/0012_add_help_text_to_base_fields.py b/backend/eurydice/destination/core/migrations/0012_add_help_text_to_base_fields.py new file mode 100644 index 0000000..f790806 --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0012_add_help_text_to_base_fields.py @@ -0,0 +1,38 @@ +# Generated by Django 3.2.6 on 2021-08-26 13:27 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("eurydice_destination_core", "0011_added_bytes_received_to_transferables"), + ] + + operations = [ + migrations.AlterField( + model_name="incomingtransferable", + name="updated_at", + field=models.DateTimeField(auto_now=True, help_text="Update date"), + ), + migrations.AlterField( + model_name="s3uploadpart", + name="created_at", + field=models.DateTimeField(auto_now_add=True, help_text="Creation date"), + ), + migrations.AlterField( + model_name="s3uploadpart", + name="updated_at", + field=models.DateTimeField(auto_now=True, help_text="Update date"), + ), + migrations.AlterField( + model_name="userprofile", + name="created_at", + field=models.DateTimeField(auto_now_add=True, help_text="Creation date"), + ), + migrations.AlterField( + model_name="userprofile", + name="updated_at", + field=models.DateTimeField(auto_now=True, help_text="Update date"), + ), + ] diff --git a/backend/eurydice/destination/core/migrations/0013_alter_incomingtransferable_created_at.py b/backend/eurydice/destination/core/migrations/0013_alter_incomingtransferable_created_at.py new file mode 100644 index 0000000..7179feb --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0013_alter_incomingtransferable_created_at.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.6 on 2021-08-30 16:07 + +import django.utils.timezone +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("eurydice_destination_core", "0012_add_help_text_to_base_fields"), + ] + + operations = [ + migrations.AlterField( + model_name="incomingtransferable", + name="created_at", + field=models.DateTimeField( + default=django.utils.timezone.now, help_text="Creation date" + ), + ), + ] diff --git a/backend/eurydice/destination/core/migrations/0014_add_removed_state_to_incoming_transferable.py b/backend/eurydice/destination/core/migrations/0014_add_removed_state_to_incoming_transferable.py new file mode 100644 index 0000000..fb65f64 --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0014_add_removed_state_to_incoming_transferable.py @@ -0,0 +1,52 @@ +# Generated by Django 3.2.7 on 2021-09-14 11:30 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("eurydice_destination_core", "0013_alter_incomingtransferable_created_at"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="incomingtransferable", + name="eurydice_destination_core_incomingtransferable_finished_at_state", + ), + migrations.AlterField( + model_name="incomingtransferable", + name="state", + field=models.CharField( + choices=[ + ("ONGOING", "Ongoing"), + ("SUCCESS", "Success"), + ("ERROR", "Error"), + ("REVOKED", "Revoked"), + ("EXPIRED", "Expired"), + ("REMOVED", "Removed"), + ], + default="ONGOING", + help_text="The state of the IncomingTransferable", + max_length=7, + verbose_name="State", + ), + ), + migrations.AddConstraint( + model_name="incomingtransferable", + constraint=models.CheckConstraint( + check=models.Q( + models.Q(("finished_at__isnull", True), ("state", "ONGOING")), + models.Q( + ("finished_at__isnull", False), + ( + "state__in", + {"SUCCESS", "REVOKED", "ERROR", "REMOVED", "EXPIRED"}, + ), + ), + _connector="OR", + ), + name="eurydice_destination_core_incomingtransferable_finished_at_state", + ), + ), + ] diff --git a/backend/eurydice/destination/core/migrations/0015_add_constraints.py b/backend/eurydice/destination/core/migrations/0015_add_constraints.py new file mode 100644 index 0000000..e7291c1 --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0015_add_constraints.py @@ -0,0 +1,75 @@ +# Generated by Django 3.2.7 on 2021-09-21 09:06 + +import django.core.validators +import django.db.models.expressions +from django.db import migrations +from django.db import models + +import eurydice.common.models.fields + + +class Migration(migrations.Migration): + dependencies = [ + ( + "eurydice_destination_core", + "0014_add_removed_state_to_incoming_transferable", + ), + ] + + operations = [ + migrations.AlterField( + model_name="incomingtransferable", + name="sha1", + field=eurydice.common.models.fields.SHA1Field( + default=None, + help_text="The SHA-1 digest of the file corresponding to the Transferable", + max_length=20, + null=True, + validators=[django.core.validators.MinLengthValidator(20)], + verbose_name="SHA-1", + ), + ), + migrations.AddConstraint( + model_name="incomingtransferable", + constraint=models.CheckConstraint( + check=models.Q( + models.Q(("state", "SUCCESS"), _negated=True), + models.Q(("sha1__isnull", False), ("sha1__length", 20)), + _connector="OR", + ), + name="eurydice_destination_core_incomingtransferable_sha1", + ), + ), + migrations.AddConstraint( + model_name="incomingtransferable", + constraint=models.CheckConstraint( + check=models.Q(("rehash_intermediary__length", 104)), + name="eurydice_destination_core_incomingtransferable_rehash_intermediary", + ), + ), + migrations.AddConstraint( + model_name="incomingtransferable", + constraint=models.CheckConstraint( + check=models.Q( + models.Q(("state", "SUCCESS"), _negated=True), + ("bytes_received", django.db.models.expressions.F("size")), + _connector="OR", + ), + name="eurydice_destination_core_incomingtransferable_bytes_received", + ), + ), + migrations.AddConstraint( + model_name="s3uploadpart", + constraint=models.CheckConstraint( + check=models.Q(("etag__length", 16)), + name="eurydice_destination_core_s3uploadpart_etag", + ), + ), + migrations.AddConstraint( + model_name="s3uploadpart", + constraint=models.CheckConstraint( + check=models.Q(("part_number__gte", 1)), + name="eurydice_destination_core_s3uploadpart_part_number", + ), + ), + ] diff --git a/backend/eurydice/destination/core/migrations/0016_incomingtransferable_eurydice_in_created_ac8963_idx.py b/backend/eurydice/destination/core/migrations/0016_incomingtransferable_eurydice_in_created_ac8963_idx.py new file mode 100644 index 0000000..31b497f --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0016_incomingtransferable_eurydice_in_created_ac8963_idx.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.9 on 2021-11-15 12:56 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("eurydice_destination_core", "0015_add_constraints"), + ] + + operations = [ + migrations.AddIndex( + model_name="incomingtransferable", + index=models.Index( + fields=["-created_at"], name="eurydice_in_created_ac8963_idx" + ), + ), + ] diff --git a/backend/eurydice/destination/core/migrations/0017_alter_incomingtransferable_index_together.py b/backend/eurydice/destination/core/migrations/0017_alter_incomingtransferable_index_together.py new file mode 100644 index 0000000..1a18cb8 --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0017_alter_incomingtransferable_index_together.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.12 on 2022-11-02 12:33 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "eurydice_destination_core", + "0016_incomingtransferable_eurydice_in_created_ac8963_idx", + ), + ] + + operations = [ + migrations.AddIndex( + model_name="incomingtransferable", + index=models.Index( + fields=["created_at", "state"], name="created_at_state_idx" + ), + ), + ] diff --git a/backend/eurydice/destination/core/migrations/0018_alter_incomingtransferable_s3_upload_id.py b/backend/eurydice/destination/core/migrations/0018_alter_incomingtransferable_s3_upload_id.py new file mode 100644 index 0000000..d26f983 --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0018_alter_incomingtransferable_s3_upload_id.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.17 on 2023-02-08 15:30 + +import django.core.validators +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("eurydice_destination_core", "0017_alter_incomingtransferable_index_together"), + ] + + operations = [ + migrations.AlterField( + model_name="incomingtransferable", + name="s3_upload_id", + field=models.CharField( + help_text="The ID for the S3 multipart upload of this IncomingTransferable.", + max_length=98, + validators=[django.core.validators.MinLengthValidator(98)], + verbose_name="S3 upload ID", + ), + ), + ] diff --git a/backend/eurydice/destination/core/migrations/0019_remove_incomingtransferable_updated_at_and_more.py b/backend/eurydice/destination/core/migrations/0019_remove_incomingtransferable_updated_at_and_more.py new file mode 100644 index 0000000..82bf5cd --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0019_remove_incomingtransferable_updated_at_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.2 on 2023-07-05 15:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("eurydice_destination_core", "0018_alter_incomingtransferable_s3_upload_id"), + ] + + operations = [ + migrations.RemoveField( + model_name="incomingtransferable", + name="updated_at", + ), + migrations.RemoveField( + model_name="s3uploadpart", + name="updated_at", + ), + migrations.RemoveField( + model_name="userprofile", + name="updated_at", + ), + ] diff --git a/backend/eurydice/destination/core/migrations/0020_user_last_access.py b/backend/eurydice/destination/core/migrations/0020_user_last_access.py new file mode 100644 index 0000000..7e7ce6a --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0020_user_last_access.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.5 on 2023-10-12 09:06 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "eurydice_destination_core", + "0019_remove_incomingtransferable_updated_at_and_more", + ), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="last_access", + field=models.DateTimeField( + blank=True, + help_text="Unlike last_login, this field gets updated every time the user accesses the API, even when they authenticate with an API token.", + null=True, + verbose_name="last access", + ), + ), + ] diff --git a/backend/eurydice/destination/core/migrations/0021_lastpacketreceivedat.py b/backend/eurydice/destination/core/migrations/0021_lastpacketreceivedat.py new file mode 100644 index 0000000..af611b4 --- /dev/null +++ b/backend/eurydice/destination/core/migrations/0021_lastpacketreceivedat.py @@ -0,0 +1,40 @@ +# Generated by Django 4.2.2 on 2023-07-28 16:10 + +import uuid + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "eurydice_destination_core", + "0020_user_last_access", + ), + ] + + operations = [ + migrations.CreateModel( + name="LastPacketReceivedAt", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "_singleton", + models.BooleanField(default=True, editable=False, unique=True), + ), + ("timestamp", models.DateTimeField(auto_now=True)), + ], + options={ + "db_table": "eurydice_last_packet_received_at", + }, + ), + ] diff --git a/backend/eurydice/destination/core/migrations/__init__.py b/backend/eurydice/destination/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/destination/core/models/__init__.py b/backend/eurydice/destination/core/models/__init__.py new file mode 100644 index 0000000..d132bb8 --- /dev/null +++ b/backend/eurydice/destination/core/models/__init__.py @@ -0,0 +1,17 @@ +"""Models used on the destination side.""" + +from .incoming_transferable import IncomingTransferable +from .incoming_transferable import IncomingTransferableState +from .last_packet_received_at import LastPacketReceivedAt +from .s3_upload_part import S3UploadPart +from .user import User +from .user import UserProfile + +__all__ = ( + "User", + "UserProfile", + "IncomingTransferable", + "IncomingTransferableState", + "S3UploadPart", + "LastPacketReceivedAt", +) diff --git a/backend/eurydice/destination/core/models/incoming_transferable.py b/backend/eurydice/destination/core/models/incoming_transferable.py new file mode 100644 index 0000000..1d5bbd6 --- /dev/null +++ b/backend/eurydice/destination/core/models/incoming_transferable.py @@ -0,0 +1,371 @@ +import hashlib +from typing import Set + +from django.conf import settings +from django.core import validators +from django.db import models +from django.db.models import F +from django.db.models import Q +from django.db.models import expressions +from django.db.models import functions +from django.db.models import query +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ + +import eurydice.common.models as common_models +import eurydice.destination.core.models as destination_models +import eurydice.destination.utils.rehash as rehash + + +class IncomingTransferableState(models.TextChoices): + """ + The set of all possible values for an IncomingTransferable's state + """ + + ONGOING = "ONGOING", _("Ongoing") + SUCCESS = "SUCCESS", _("Success") + ERROR = "ERROR", _("Error") + REVOKED = "REVOKED", _("Revoked") + EXPIRED = "EXPIRED", _("Expired") + REMOVED = "REMOVED", _("Removed") + + @classmethod + def get_final_states(cls) -> Set["IncomingTransferableState"]: + """List final states. + + Returns: a set containing the final states. + + """ + return { # pytype: disable=bad-return-type + cls.ERROR, + cls.EXPIRED, + cls.REVOKED, + cls.SUCCESS, + cls.REMOVED, + } + + @property + def is_final(self) -> bool: + """Check that the state is terminal. + + Returns: True if the state is final, False otherwise. + + """ + return self in self.get_final_states() + + +S3_UPLOAD_ID_LENGTH = 98 + + +def _build_progress_annotation() -> expressions.Case: + """Build the annotation for computing the progress when querying the Transferable(s). + + - if a transferable has not fully been submitted, progress is None + - else progress is the percentage of the file that was received until now + + Returns: + Case: the Django ORM conditions for computing the transferable's progress + """ + return expressions.Case( + expressions.When( + size__isnull=True, + then=None, + ), + # denominator will be None if size is 0, in this case, progress must be 100 + default=functions.Coalesce( + models.F("bytes_received") * 100 / functions.NullIf(models.F("size"), 0), + 100, + ), + output_field=models.PositiveSmallIntegerField(null=True), + ) + + +def _build_expires_at_annotation() -> expressions.Case: + """ + Build the annotation for computing the expiration date when querying the + Transferable. + + Returns: + Case: the Django ORM conditions for computing the transferable's expiration date + """ + return expressions.Case( + expressions.When( + models.Q(finished_at__isnull=True) + | ~models.Q(state=IncomingTransferableState.SUCCESS), + then=None, + ), + default=models.F("finished_at") + settings.S3REMOVER_EXPIRE_TRANSFERABLES_AFTER, + output_field=models.DateTimeField(null=True), + ) + + +class TransferableManager(models.Manager): + def get_queryset(self) -> query.QuerySet["IncomingTransferable"]: + """ + Compute the progress for the queried Transferable(s) and + annotate the queryset with it. + + Returns: + The queryset annotated with its progress + """ + + return ( + super() + .get_queryset() + .annotate( + progress=_build_progress_annotation(), + expires_at=_build_expires_at_annotation(), + ) + ) + + +class IncomingTransferable(common_models.AbstractBaseModel): + """ + A Transferable sent to the destination i.e. a file + received or being received on the destination side. + """ + + objects = TransferableManager() + + name = common_models.TransferableNameField( + verbose_name=_("Name"), + help_text=_("The name of the file corresponding to the Transferable"), + ) + sha1 = common_models.SHA1Field( + verbose_name=_("SHA-1"), + help_text=_("The SHA-1 digest of the file corresponding to the Transferable"), + null=True, + default=None, + ) + rehash_intermediary = models.BinaryField( + validators=[ + validators.MaxLengthValidator(rehash.SHA1_HASHLIB_BUFSIZE), + validators.MinLengthValidator(rehash.SHA1_HASHLIB_BUFSIZE), + ], + verbose_name=_("Rehash intermediary"), + help_text=_( + "Bytes corresponding to the internal state of the hashlib object " + "as returned by eurydice.destination.utils.rehash.sha1_to_bytes" + ), + default=rehash.sha1_to_bytes(hashlib.sha1(b"")), # nosec + ) + bytes_received = common_models.TransferableSizeField( + verbose_name=_("Amount of bytes received"), + help_text=_("The amount of bytes received until now for the Transferable"), + default=0, + ) + size = common_models.TransferableSizeField( + verbose_name=_("Size in bytes"), + help_text=_("The size in bytes of the file corresponding to the Transferable"), + null=True, + default=None, + ) + s3_bucket_name = common_models.S3BucketNameField( + verbose_name=_("S3 bucket name"), + help_text=_( + "The name of the S3 bucket containing the file corresponding to this " + "IncomingTransferable" + ), + ) + s3_object_name = common_models.S3ObjectNameField( + verbose_name=_("S3 object name"), + help_text=_( + "The name of the S3 object holding the data corresponding to this " + "IncomingTransferable" + ), + ) + s3_upload_id = models.CharField( + validators=[validators.MinLengthValidator(S3_UPLOAD_ID_LENGTH)], + max_length=S3_UPLOAD_ID_LENGTH, + verbose_name=_("S3 upload ID"), + help_text=_("The ID for the S3 multipart upload of this IncomingTransferable."), + ) + user_profile = models.ForeignKey( + "eurydice_destination_core.UserProfile", + on_delete=models.RESTRICT, + verbose_name=_("User profile"), + help_text=_("The profile of the user owning the Transferable"), + ) + user_provided_meta = common_models.UserProvidedMetaField( + verbose_name=_("User provided metadata"), + help_text=_("The metadata provided by the user on file submission"), + default=dict, + ) + state = models.CharField( + max_length=7, + choices=IncomingTransferableState.choices, + default=IncomingTransferableState.ONGOING, + verbose_name=_("State"), + help_text=_("The state of the IncomingTransferable"), + ) + finished_at = models.DateTimeField( + null=True, + verbose_name=_("Transfer finish date"), + help_text=_( + "A timestamp indicating the end of the reception of the " + "IncomingTransferable" + ), + ) + created_at = models.DateTimeField( + default=timezone.now, + help_text=common_models.AbstractBaseModel.created_at.field.help_text, # type: ignore # noqa: E501 + ) + + def _clear_multipart_data(self) -> None: + """Delete S3 Upload Parts from the database for this IncomingTransferable. + + This method is meant to clear entries from the database that point to parts of + a multipart upload for a transferable that has no data anymore. + + This method should not be called if the state of the IncomingTransferable + is SUCCESS. + """ + destination_models.S3UploadPart.objects.filter( + incoming_transferable=self + ).delete() + + def _mark_as_finished( + self, + state: IncomingTransferableState, + save: bool = True, + remove_s3_uploaded_parts: bool = False, + ) -> None: + """Mark the IncomingTransferable as `state` and set its finish date to now. + + Args: + state: the IncomingTransferableState the IncomingTransferable must be + set to. + save: boolean indicating whether changed fields should be saved or not. + remove_s3_uploaded_parts: boolean indicating whether associated + S3UploadParts should be deleted or not. + """ + self.state = state + self.finished_at = timezone.now() + + if save: + self.save(update_fields=["state", "finished_at"]) + + if remove_s3_uploaded_parts: + self._clear_multipart_data() + + def mark_as_error(self, save: bool = True) -> None: + """Mark the IncomingTransferable as ERROR and set its finish date to now. + + Args: + save: boolean indicating whether changes should be saved to database + or simply edited in the ORM instance. + """ + self._mark_as_finished( # pytype: disable=wrong-arg-types + IncomingTransferableState.ERROR, + save, + remove_s3_uploaded_parts=True, + ) + + def mark_as_revoked(self, save: bool = True) -> None: + """Mark the IncomingTransferable as REVOKED and set its finish date to now. + + Args: + save: boolean indicating whether changes should be saved to database + or simply edited in the ORM instance. + """ + self._mark_as_finished( # pytype: disable=wrong-arg-types + IncomingTransferableState.REVOKED, + save, + remove_s3_uploaded_parts=True, + ) + + def mark_as_success(self, save: bool = True) -> None: + """Mark the IncomingTransferable as SUCCESS and set its finish date to now. + + Args: + save: boolean indicating whether changes should be saved to database + or simply edited in the ORM instance. + """ + self._mark_as_finished( # pytype: disable=wrong-arg-types + IncomingTransferableState.SUCCESS, + save, + remove_s3_uploaded_parts=False, + ) + + def mark_as_expired(self, save: bool = True) -> None: + """Mark the IncomingTransferable as EXPIRED. + + Args: + save: boolean indicating whether changes should be saved to database + or simply edited in the ORM instance. + """ + self.state = IncomingTransferableState.EXPIRED + + if save: + self.save(update_fields=["state"]) + + self._clear_multipart_data() + + def mark_as_removed(self, save: bool = True) -> None: + """Mark the IncomingTransferable as REMOVED. + + Args: + save: boolean indicating whether changes should be saved to database + or simply edited in the ORM instance. + """ + self.state = IncomingTransferableState.REMOVED + + if save: + self.save(update_fields=["state"]) + + self._clear_multipart_data() + + class Meta: + db_table = "eurydice_incoming_transferables" + indexes = [ + models.Index( + fields=[ + "-created_at", + ] + ), + models.Index(fields=["created_at", "state"], name="created_at_state_idx"), + ] + constraints = [ + models.UniqueConstraint( + name="%(app_label)s_%(class)s_s3_bucket_name_s3_object_name", + fields=["s3_bucket_name", "s3_object_name"], + condition=~models.Q(s3_bucket_name="", s3_object_name=""), + ), + models.CheckConstraint( + name="%(app_label)s_%(class)s_finished_at_state", + check=( + Q( + finished_at__isnull=True, + state=IncomingTransferableState.ONGOING, + ) + | Q( + finished_at__isnull=False, + state__in=IncomingTransferableState.get_final_states(), + ) + ), + ), + models.CheckConstraint( + name="%(app_label)s_%(class)s_sha1", + check=~Q(state=IncomingTransferableState.SUCCESS) + | Q( + sha1__isnull=False, + sha1__length=common_models.SHA1Field.DIGEST_SIZE_IN_BYTES, + ), + ), + models.CheckConstraint( + name="%(app_label)s_%(class)s_rehash_intermediary", + check=Q(rehash_intermediary__length=rehash.SHA1_HASHLIB_BUFSIZE), + ), + models.CheckConstraint( + name="%(app_label)s_%(class)s_bytes_received", + check=~Q(state=IncomingTransferableState.SUCCESS) + | Q(bytes_received=F("size")), + ), + models.CheckConstraint( + name="%(app_label)s_%(class)s_finished_at_created_at", + check=Q(finished_at__gte=F("created_at")), + ), + ] + + +__all__ = ("IncomingTransferable", "IncomingTransferableState") diff --git a/backend/eurydice/destination/core/models/last_packet_received_at.py b/backend/eurydice/destination/core/models/last_packet_received_at.py new file mode 100644 index 0000000..cad9b91 --- /dev/null +++ b/backend/eurydice/destination/core/models/last_packet_received_at.py @@ -0,0 +1,8 @@ +from eurydice.common import models as common_models + + +class LastPacketReceivedAt(common_models.TimestampSingleton): + """Singleton holding the date of the last received packet (data or heartbeat).""" + + class Meta: + db_table = "eurydice_last_packet_received_at" diff --git a/backend/eurydice/destination/core/models/s3_upload_part.py b/backend/eurydice/destination/core/models/s3_upload_part.py new file mode 100644 index 0000000..8e2cc79 --- /dev/null +++ b/backend/eurydice/destination/core/models/s3_upload_part.py @@ -0,0 +1,56 @@ +from django.core import validators +from django.db import models +from django.db.models import Q +from django.utils.translation import gettext_lazy as _ + +import eurydice.common.models as common_models + +_S3_PART_ETAG_LENGTH = 16 # bytes + + +class S3UploadPart(common_models.AbstractBaseModel): + """Model representing a part in an S3 multipart upload.""" + + etag = models.BinaryField( + max_length=_S3_PART_ETAG_LENGTH, + validators=[ + validators.MinLengthValidator(_S3_PART_ETAG_LENGTH), + ], + verbose_name=_("S3 multipart upload part ETag"), + help_text=_( + "Bytes corresponding to the MD5 sum for the upload part. " + "Note that S3 multipart upload parts' ETags should always be an MD5 sum. " + "https://docs.aws.amazon.com/AmazonS3/latest/API/API_Object.html" + ), + ) + part_number = models.PositiveIntegerField( + validators=[ + validators.MinValueValidator(1), + ], + verbose_name=_("S3 multipart upload part number"), + help_text=_("The index of this part within the multipart upload."), + ) + incoming_transferable = models.ForeignKey( + "eurydice_destination_core.IncomingTransferable", + on_delete=models.RESTRICT, + related_name="s3_upload_parts", + verbose_name=_("Incoming Transferable"), + help_text=_("The profile of the user owning the Transferable"), + ) + + class Meta: + db_table = "eurydice_s3_upload_part" + constraints = [ + models.UniqueConstraint( + name="%(app_label)s_%(class)s_incoming_transferable_part_number", + fields=["incoming_transferable", "part_number"], + ), + models.CheckConstraint( + name="%(app_label)s_%(class)s_etag", + check=Q(etag__length=_S3_PART_ETAG_LENGTH), + ), + models.CheckConstraint( + name="%(app_label)s_%(class)s_part_number", + check=Q(part_number__gte=1), + ), + ] diff --git a/backend/eurydice/destination/core/models/user.py b/backend/eurydice/destination/core/models/user.py new file mode 100644 index 0000000..f8c4d58 --- /dev/null +++ b/backend/eurydice/destination/core/models/user.py @@ -0,0 +1,34 @@ +from django.conf import settings +from django.db import models +from django.utils.translation import gettext_lazy as _ + +import eurydice.common.models as common_models + + +class User(common_models.AbstractUser): + """A user on the destination side.""" + + +class UserProfile(common_models.AbstractBaseModel): + """User profile metadata.""" + + user = models.OneToOneField( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + null=True, + related_name="user_profile", + ) + associated_user_profile_id = models.UUIDField( # noqa: DJ01 + unique=True, + verbose_name=_("Associated user profile ID"), + help_text=_( + "The UUID of the user profile on the origin side that is " + "associated with the user profile on this side" + ), + ) + + class Meta: + db_table = "eurydice_user_profiles" + + +__all__ = ("User", "UserProfile") diff --git a/backend/eurydice/destination/receiver/__init__.py b/backend/eurydice/destination/receiver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/destination/receiver/__main__.py b/backend/eurydice/destination/receiver/__main__.py new file mode 100644 index 0000000..6b75530 --- /dev/null +++ b/backend/eurydice/destination/receiver/__main__.py @@ -0,0 +1,13 @@ +if __name__ == "__main__": + import os + + import django + + os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", "eurydice.destination.config.settings.base" + ) + django.setup() + + from eurydice.destination.receiver import main + + main.run() diff --git a/backend/eurydice/destination/receiver/main.py b/backend/eurydice/destination/receiver/main.py new file mode 100644 index 0000000..637b09e --- /dev/null +++ b/backend/eurydice/destination/receiver/main.py @@ -0,0 +1,91 @@ +import datetime +import logging + +import humanfriendly as hf +from django.conf import settings +from django.db import connections +from django.utils import timezone + +from eurydice.common import protocol +from eurydice.common.utils import signals +from eurydice.destination.core.models import LastPacketReceivedAt +from eurydice.destination.receiver import packet_handler +from eurydice.destination.receiver import packet_receiver + +logging.config.dictConfig(settings.LOGGING) # type: ignore +logger = logging.getLogger(__name__) + + +class _PacketLogger: + """Log received and missed packets.""" + + def __init__(self) -> None: + self._last_log_at: datetime.datetime = timezone.now() + + def log_received(self, packet: protocol.OnTheWirePacket) -> None: + """Log the reception of a packet.""" + if packet.is_empty(): + logger.info("Heartbeat received") + else: + logger.info(f"{packet} received") + + self._last_log_at = timezone.now() + + def log_not_received(self) -> None: + """Log an error when no packet is received in the time interval defined in + `settings.EXPECT_PACKET_EVERY`. + """ + now = timezone.now() + + if now - self._last_log_at >= settings.EXPECT_PACKET_EVERY: + logger.error( + "No data packet or heartbeat received in the last " + f"{hf.format_timespan(settings.EXPECT_PACKET_EVERY)}. " + "Check the health of the sender on the origin side." + ) + + self._last_log_at = now + + +def _loop() -> None: + """Loop indefinitely until interrupted, receive packets as they become available, + and log an error when no data packet or heartbeat is received in the defined time + interval. + """ + with packet_receiver.PacketReceiver() as receiver: + handler = packet_handler.OnTheWirePacketHandler() + keep_running = signals.BooleanCondition() + packet_logger = _PacketLogger() + + logger.info("Ready to receive OnTheWirePackets") + + while True: + try: + try: + packet = receiver.receive(timeout=settings.PACKET_RECEIVER_TIMEOUT) + except packet_receiver.NothingToReceive: + if not keep_running: + break + + packet_logger.log_not_received() + except packet_receiver.ReceptionError: + logger.exception("Error on packet reception.") + else: + LastPacketReceivedAt.update() + packet_logger.log_received(packet) + handler.handle(packet) + except Exception: + logger.exception( + "An unexpected error occurred while processing an OnTheWirePacket" + ) + + +def run() -> None: # pragma: no cover + """Entrypoint for the receiver.""" + try: + _loop() + finally: + connections.close_all() + + +__all__ = ("run",) diff --git a/backend/eurydice/destination/receiver/packet_handler/__init__.py b/backend/eurydice/destination/receiver/packet_handler/__init__.py new file mode 100644 index 0000000..5f896b7 --- /dev/null +++ b/backend/eurydice/destination/receiver/packet_handler/__init__.py @@ -0,0 +1,3 @@ +from .packet_handler import OnTheWirePacketHandler + +__all__ = ("OnTheWirePacketHandler",) diff --git a/backend/eurydice/destination/receiver/packet_handler/extractors/__init__.py b/backend/eurydice/destination/receiver/packet_handler/extractors/__init__.py new file mode 100644 index 0000000..10e583e --- /dev/null +++ b/backend/eurydice/destination/receiver/packet_handler/extractors/__init__.py @@ -0,0 +1,17 @@ +from .history import OngoingHistoryExtractor +from .transferable_range import FinalDigestMismatchError +from .transferable_range import FinalSizeMismatchError +from .transferable_range import MissedTransferableRangeError +from .transferable_range import TransferableRangeExtractionError +from .transferable_range import TransferableRangeExtractor +from .transferable_revocation import TransferableRevocationExtractor + +__all__ = ( + "OngoingHistoryExtractor", + "TransferableRangeExtractor", + "TransferableRevocationExtractor", + "TransferableRangeExtractionError", + "MissedTransferableRangeError", + "FinalDigestMismatchError", + "FinalSizeMismatchError", +) diff --git a/backend/eurydice/destination/receiver/packet_handler/extractors/base.py b/backend/eurydice/destination/receiver/packet_handler/extractors/base.py new file mode 100644 index 0000000..7331ab3 --- /dev/null +++ b/backend/eurydice/destination/receiver/packet_handler/extractors/base.py @@ -0,0 +1,23 @@ +import abc + +import eurydice.common.protocol as protocol + + +class OnTheWirePacketExtractor(abc.ABC): + """ + Abstract class for OnTheWirePacketExtractors. + """ + + @abc.abstractmethod + def extract(self, packet: protocol.OnTheWirePacket) -> None: + """ + Subclasses should implement this method to extract the required data + from the given packet. + + Args: + packet: packet to process. + """ + raise NotImplementedError + + +__all__ = ("OnTheWirePacketExtractor",) diff --git a/backend/eurydice/destination/receiver/packet_handler/extractors/history.py b/backend/eurydice/destination/receiver/packet_handler/extractors/history.py new file mode 100644 index 0000000..daa60ec --- /dev/null +++ b/backend/eurydice/destination/receiver/packet_handler/extractors/history.py @@ -0,0 +1,159 @@ +import collections +import logging +import uuid +from typing import Dict +from typing import Set + +from django.conf import settings +from django.utils import timezone + +from eurydice.common import protocol +from eurydice.destination.core import models +from eurydice.destination.receiver.packet_handler import s3_helpers +from eurydice.destination.receiver.packet_handler.extractors import base +from eurydice.destination.storage import fs + +UUIDSet = Set[uuid.UUID] + +logger = logging.getLogger(__name__) + + +class _HistoryEntryMap(collections.UserDict): + """A convenient representation of a History object that maps transferable IDs + to HistoryEntries. + + Args: + history: the History object to convert to a mapping. + + """ + + def __init__(self, history: protocol.History) -> None: + super().__init__() + self.data: Dict[uuid.UUID, protocol.HistoryEntry] = { + e.transferable_id: e for e in history.entries + } + + +def _filter(transferable_ids: UUIDSet, **kwargs) -> UUIDSet: + """Select the `transferable_ids` corresponding to IncomingTransferables in + the database using, in addition, filter parameters provided as kwargs.""" + results = models.IncomingTransferable.objects.filter( + id__in=transferable_ids, **kwargs + ).values_list("id", flat=True) + + return set(results) # type: ignore + + +def _list_ongoing_transferable_ids(transferable_ids: UUIDSet) -> UUIDSet: + """Select the IncomingTransferable that have their id in `transferable_ids` and + that have the state ONGOING. + """ + return _filter(transferable_ids, state=models.IncomingTransferableState.ONGOING) + + +def _list_finished_transferable_ids(transferable_ids: UUIDSet) -> UUIDSet: + """Select the IncomingTransferable that have their id in `transferable_ids` and + that have been completely processed (i.e. with a final state). + """ + return _filter( + transferable_ids, state__in=models.IncomingTransferableState.get_final_states() + ) + + +def _list_missed_transferable_ids( + all_transferable_ids: UUIDSet, ongoing_transferable_ids: UUIDSet +) -> UUIDSet: + """Select the IncomingTransferable that have their id in `all_transferable_ids` + and that do not appear in the database. + """ + finished_transferables_ids = _list_finished_transferable_ids(all_transferable_ids) + return all_transferable_ids - ongoing_transferable_ids - finished_transferables_ids + + +def _process_ongoing_transferables(ongoing_transferable_ids: UUIDSet) -> None: + """Abort the multipart uploads of the ONGOING transferables and mark the objects + as ERROR. + """ + for transferable_id in ongoing_transferable_ids: + transferable = models.IncomingTransferable.objects.get(id=transferable_id) + if settings.MINIO_ENABLED: + s3_helpers.abort_multipart_upload(transferable) + else: + fs.delete(transferable) + transferable.mark_as_error() + + logger.error( + f"According to history, transferable '{transferable.id}' is in a " + f"final state, but it was not the case receiver-side: marked this transfer " + f"as failure and removed its parts from storage (if it had any)." + ) + + +def _process_missed_transferables( + missed_transferable_ids: UUIDSet, history_entry_map: _HistoryEntryMap +) -> None: + """Create IncomingTransferable ERROR entries in the database to record transferables + that did not reach the destination side. + """ + for missed_id in missed_transferable_ids: + user_profile, _ = models.UserProfile.objects.get_or_create( + associated_user_profile_id=history_entry_map[missed_id].user_profile_id + ) + + now = timezone.now() + models.IncomingTransferable.objects.create( + id=missed_id, + name=history_entry_map[missed_id].name, + sha1=history_entry_map[missed_id].sha1, + bytes_received=0, + size=None, + s3_bucket_name="", + s3_object_name="", + s3_upload_id="", + user_profile=user_profile, + user_provided_meta=history_entry_map[missed_id].user_provided_meta or {}, + created_at=now, + finished_at=now, + state=models.IncomingTransferableState.ERROR, + ) + + logger.info( + f"The IncomingTransferable {missed_id} has been created in database " + f"with the state ERROR." + ) + + +def _process_history(history: protocol.History) -> None: + """Abort ONGOING IncomingTransferable that appear in the history and record in the + database transferables that did not reach the destination side. + """ + history_entry_map = _HistoryEntryMap(history) + transferable_ids = set(history_entry_map.keys()) + + ongoing_transferable_ids = _list_ongoing_transferable_ids(transferable_ids) + missed_transferable_ids = _list_missed_transferable_ids( + transferable_ids, ongoing_transferable_ids + ) + + _process_ongoing_transferables(ongoing_transferable_ids) + _process_missed_transferables(missed_transferable_ids, history_entry_map) + + +class OngoingHistoryExtractor(base.OnTheWirePacketExtractor): + """Process the history packaged in an OnTheWirePacket.""" + + def extract(self, packet: protocol.OnTheWirePacket) -> None: + """Entrypoint of the OnTheWirePacketExtractor. Process the history in an + OnTheWirePacket if there is one. + + Args: + packet: the OnTheWirePacket containing the history to process. + + """ + if packet.history: + logger.debug("Start processing history.") + _process_history(packet.history) + logger.info("History processed.") + + +__all__ = ("OngoingHistoryExtractor",) diff --git a/backend/eurydice/destination/receiver/packet_handler/extractors/transferable_range.py b/backend/eurydice/destination/receiver/packet_handler/extractors/transferable_range.py new file mode 100644 index 0000000..f2c906e --- /dev/null +++ b/backend/eurydice/destination/receiver/packet_handler/extractors/transferable_range.py @@ -0,0 +1,317 @@ +import hashlib +import logging +import types +from typing import Tuple + +import humanfriendly as hf +from django.conf import settings + +import eurydice.common.protocol as protocol +import eurydice.destination.core.models as models +import eurydice.destination.receiver.packet_handler.extractors.base as base_extractor +import eurydice.destination.receiver.transferable_ingestion as transferable_ingestion +import eurydice.destination.receiver.transferable_ingestion_fs as transferable_ingestion_fs # noqa: E501 +import eurydice.destination.utils.rehash as rehash + +logger = logging.getLogger(__name__) + + +class TransferableRangeExtractionError(RuntimeError): + """ + Base Exception for errors while extracting a TransferableRange from an + OnTheWirePacket. + """ + + +class MissedTransferableRangeError(TransferableRangeExtractionError): + """The received TransferableRange's byte offset is not the expected one.""" + + +class FinalDigestMismatchError(TransferableRangeExtractionError): + """ + The received successful IncomingTransferable's final digest + does not match the one received in the OnTheWirePacket. + """ + + +class FinalSizeMismatchError(TransferableRangeExtractionError): + """ + The received successful IncomingTransferable's final size + does not match the one received in the OnTheWirePacket. + """ + + +class TransferableAlreadyInFinalState(TransferableRangeExtractionError): + """ + Received a TransferableRange for an IncomingTransferable that is already + in a final state. + """ + + +def _get_or_create_transferable( + transferable_range: protocol.TransferableRange, +) -> models.IncomingTransferable: + """ + Given a TransferableRange found in an OnTheWirePacket, get or create the associated + IncomingTransferable. Only the transferable ID is used if the IncomingTransferable + already existed (other fields are ignored). + + If the TransferableRange references an unknown user profile, it is also created. + + Args: + transferable_range: the TransferableRange (its data may be used for creations). + + Returns: + The IncomingTransferable ORM instance corresponding to the given range. + + """ + user_profile, _ = models.UserProfile.objects.get_or_create( + associated_user_profile_id=transferable_range.transferable.user_profile_id + ) + + transferable, _ = models.IncomingTransferable.objects.get_or_create( + id=transferable_range.transferable.id, + defaults={ + "id": transferable_range.transferable.id, + "user_profile": user_profile, + "name": transferable_range.transferable.name, + "bytes_received": 0, + "size": transferable_range.transferable.size, + "sha1": None, + "s3_bucket_name": settings.MINIO_BUCKET_NAME, + "s3_object_name": str(transferable_range.transferable.id), + "s3_upload_id": "", + "user_provided_meta": transferable_range.transferable.user_provided_meta, + }, + ) + + return transferable + + +def _assert_no_transferable_ranges_were_missed( + transferable_range: protocol.TransferableRange, + transferable: models.IncomingTransferable, +) -> None: + """ + Given a TransferableRange and the number of bytes received so far + for the associated IncomingTransferable, check if a TransferableRange + was missed. + + Args: + transferable_range: the supposed next TransferableRange. + transferable: the associated IncomingTransferable. + + Raises: + MissedTransferableRangeError when a TransferableRange was missed. + + """ + expected_byte_offset = transferable.bytes_received + if expected_byte_offset != transferable_range.byte_offset: + raise MissedTransferableRangeError( + f"Expected byte offset {transferable.bytes_received} but got " + f"TransferableRange with byte offset {transferable_range.byte_offset} for " + f"Transferable {transferable_range.transferable.id}" + ) + + +def _assert_transferable_is_ready(transferable: models.IncomingTransferable) -> None: + """ + Given an IncomingTransferable, make sure it is ready to ingest new data, i.e. it + is still in the ONGOING state. + + Args: + transferable: an IncomingTransferable whose state should be ONGOING. + + Raises: + TransferableAlreadyInFinalState when the Transferable could not receive new + data. + + """ + if transferable.state != models.IncomingTransferableState.ONGOING: + raise TransferableAlreadyInFinalState( + f"Received TransferableRange for Transferable {transferable.id} which" + f" is not currently in an ONGOING state: {transferable.state}." + ) + + +def _assert_transferable_size_is_consistent( + transferable_range: protocol.TransferableRange, + transferable: models.IncomingTransferable, +) -> None: + """Given a received TransferableRange and its associated IncomingTransferable, + raise an exception and log an error if the number of bytes received is unexpected. + + NOTE: this function is supposed to be called on the last TransferableRange. + + Args: + transferable_range: the received TransferableRange. + transferable: the associated IncomingTransferable. + + Raises: + FinalSizeMismatchError: when the number of bytes received for the Transferable + is unexpected. + + """ + bytes_received = transferable.bytes_received + len(transferable_range.data) + bytes_expected = transferable_range.transferable.size + + if bytes_received != bytes_expected: + raise FinalSizeMismatchError( + f"Received {hf.format_size(bytes_received)}, " + f"expected {hf.format_size(bytes_expected)} " + f"for Transferable {transferable_range.transferable.id}" + ) + + if transferable.size is not None and transferable.size != bytes_expected: + logger.warning( + f"IncomingTransferable {transferable.id} was initially announced with " + f"size {transferable.size}o but final size is {bytes_received}o." + ) + + +def _assert_transferable_sha1_is_consistent( + transferable_range: protocol.TransferableRange, + computed_sha1: "hashlib._Hash", +) -> None: + """Given a received TransferableRange and the sha1 computed on the bytes received, + raise an exception and log an error if the computed and expected digest diverge. + + NOTE: this function is supposed to be called on the last TransferableRange. + + Args: + transferable_range: the received TransferableRange. + computed_sha1: the computed SHA1 from all previous ranges for this Transferable. + + Raises: + FinalDigestMismatchError: when the two digests don't match + + """ + if computed_sha1.digest() != transferable_range.transferable.sha1: + # in final TransferableRange the sha1 attribute should always be present + expected_sha1_hex = transferable_range.transferable.sha1.hex() # type: ignore + computed_sha1_hex = computed_sha1.hexdigest() + + raise FinalDigestMismatchError( + f"Computed digest for Transferable {transferable_range.transferable.id} " + f"was {computed_sha1_hex} expected {expected_sha1_hex}" + ) + + +def _extract_data( + transferable_range: protocol.TransferableRange, + transferable: models.IncomingTransferable, +) -> Tuple[bytes, "hashlib._Hash"]: + """ + Given a TransferableRange and its associated Transferable database entry, returns + the TransferableRange's data and an updated version of its SHA1 hash. + + Raises: + RangeSizeMismatchError when TransferableRange's data is not the right length. + + Args: + transferable_range: the TransferableRange to get data from. + transferable: the database entry used to retrieve the current SHA1. + + """ + sha1 = rehash.sha1_from_bytes(bytes(transferable.rehash_intermediary)) + sha1.update(transferable_range.data) + + return transferable_range.data, sha1 + + +def _prepare_ingestion( + source: protocol.TransferableRange, + destination: models.IncomingTransferable, +) -> transferable_ingestion.PendingIngestionData: + """ + Given a TransferableRange (the source), make sure it is valid and ready to be + incorporated to the associated IncomingTransferable (the destination). + + Args: + source: the TransferableRange to read information from. + destination: the push new data to. + + """ + _assert_transferable_is_ready(destination) + _assert_no_transferable_ranges_were_missed(source, destination) + + data, computed_sha1 = _extract_data(source, destination) + + if source.is_last: + _assert_transferable_size_is_consistent(source, destination) + _assert_transferable_sha1_is_consistent(source, computed_sha1) + + return transferable_ingestion.PendingIngestionData( + data=data, + sha1=computed_sha1, + eof=source.is_last, + ) + + +def _extract_transferable_range(transferable_range: protocol.TransferableRange) -> None: + """Extract a single transferable range. + + Args: + transferable_range: the transferable range to process. + + """ + logger.info(f"Extracting data for {transferable_range.transferable.id}") + + transferable = _get_or_create_transferable(transferable_range) + + ingestion: types.ModuleType + if settings.MINIO_ENABLED: + ingestion = transferable_ingestion + else: + ingestion = transferable_ingestion_fs + + if transferable.state == models.IncomingTransferableState.ERROR: + logger.info( + f"IncomingTransferable {transferable.id} has state " + f"{models.IncomingTransferableState.ERROR.value}. " # pytype: disable=attribute-error # noqa: E501 + "Ignoring the associated transferable range received." + ) + return + + try: + to_ingest = _prepare_ingestion( + source=transferable_range, + destination=transferable, + ) + + ingestion.ingest(transferable, to_ingest) + + logger.info( + f"Successfully extracted and ingested TransferableRange for " + f"{transferable_range.transferable.id}" + ) + + if transferable.state == models.IncomingTransferableState.SUCCESS: + logger.info(f"IncomingTransferable {transferable.id} fully received") + + except Exception: + ingestion.abort_ingestion(transferable) + logger.exception( + f"Encountered an error when trying to extract and ingest " + f"transferable range for transferable {transferable.id}" + f"from an OnTheWirePacket." + ) + + +class TransferableRangeExtractor(base_extractor.OnTheWirePacketExtractor): + """ + Object to extract TransferableRanges from a given OnTheWirePacket. + """ + + def extract(self, packet: protocol.OnTheWirePacket) -> None: + """Given an OnTheWirePacket, extract all its TransferableRanges. + + Args: + packet: packet to extract TransferableRanges from. + + """ + for transferable_range in packet.transferable_ranges: + _extract_transferable_range(transferable_range) + + +__all__ = ("TransferableRangeExtractor",) diff --git a/backend/eurydice/destination/receiver/packet_handler/extractors/transferable_revocation.py b/backend/eurydice/destination/receiver/packet_handler/extractors/transferable_revocation.py new file mode 100644 index 0000000..0e25898 --- /dev/null +++ b/backend/eurydice/destination/receiver/packet_handler/extractors/transferable_revocation.py @@ -0,0 +1,99 @@ +import logging + +from django.utils import timezone + +from eurydice.common import protocol +from eurydice.destination.core import models +from eurydice.destination.receiver.packet_handler import s3_helpers +from eurydice.destination.receiver.packet_handler.extractors import base + +logger = logging.getLogger(__name__) + + +def _create_revoked_transferable(revocation: protocol.TransferableRevocation) -> None: + """Create an IncomingTransferable object with the state REVOKED in the database. + + If a UserProfile corresponding to the `user_profile_id` in the revocation does not + exist, it will be created. + """ + user_profile, _ = models.UserProfile.objects.get_or_create( + associated_user_profile_id=revocation.user_profile_id + ) + + now = timezone.now() + models.IncomingTransferable.objects.create( + id=revocation.transferable_id, + name=revocation.transferable_name, + sha1=revocation.transferable_sha1, + bytes_received=0, + size=None, + s3_bucket_name="", + s3_object_name="", + s3_upload_id="", + user_profile=user_profile, + user_provided_meta={}, + created_at=now, + finished_at=now, + state=models.IncomingTransferableState.REVOKED, + ) + + +def _revoke_transferable(transferable: models.IncomingTransferable) -> None: + """Mark a transferable as REVOKED in the database and remove its data from the + storage. + """ + s3_helpers.abort_multipart_upload(transferable) + transferable.mark_as_revoked() + + +def _process_revocation(revocation: protocol.TransferableRevocation) -> None: + """Attempt to mark an IncomingTransferable as REVOKED and to remove its data. + + Args: + revocation: the TransferableRevocation to process. + + """ + try: + transferable = models.IncomingTransferable.objects.get( + id=revocation.transferable_id + ) + except models.IncomingTransferable.DoesNotExist: + _create_revoked_transferable(revocation) + else: + if transferable.state != models.IncomingTransferableState.ONGOING: + logger.error( + f"The IncomingTransferable {transferable.id} cannot be revoked as " + f"its state is {transferable.state}. " + f"Only {models.IncomingTransferableState.ONGOING.value} transferables " # pytype: disable=attribute-error # noqa: E501 + f"can be revoked." + ) + return + + _revoke_transferable(transferable) + + logger.info( + f"The IncomingTransferable {revocation.transferable_id} has been marked as " + f"REVOKED and its data removed from the storage (if it had any)." + ) + + +class TransferableRevocationExtractor(base.OnTheWirePacketExtractor): + """Process the transferable revocations in an OnTheWirePacket.""" + + def extract(self, packet: protocol.OnTheWirePacket) -> None: + """Sequentially process the transferable revocations in an OnTheWirePacket, + and log encountered errors. + + Args: + packet: the OnTheWirePacket to extract the revocations from. + + """ + + for revocation in packet.transferable_revocations: + try: + _process_revocation(revocation) + except Exception: + logger.exception(f"Cannot process revocation '{revocation}'.") + + +__all__ = ("TransferableRevocationExtractor",) diff --git a/backend/eurydice/destination/receiver/packet_handler/packet_handler.py b/backend/eurydice/destination/receiver/packet_handler/packet_handler.py new file mode 100644 index 0000000..afc5666 --- /dev/null +++ b/backend/eurydice/destination/receiver/packet_handler/packet_handler.py @@ -0,0 +1,26 @@ +from eurydice.common import protocol +from eurydice.destination.receiver.packet_handler import extractors + + +class OnTheWirePacketHandler: + """Allows for the processing of received OnTheWirePackets.""" + + def __init__(self) -> None: + self._extractors = ( + extractors.TransferableRangeExtractor(), + extractors.TransferableRevocationExtractor(), + extractors.OngoingHistoryExtractor(), + ) + + def handle(self, packet: protocol.OnTheWirePacket) -> None: + """Handle a received OnTheWire packet by processing it using extractors. + + Args: + packet: the received OnTheWirePacket to handle. + + """ + for extractor in self._extractors: + extractor.extract(packet) + + +__all__ = ("OnTheWirePacketHandler",) diff --git a/backend/eurydice/destination/receiver/packet_handler/s3_helpers.py b/backend/eurydice/destination/receiver/packet_handler/s3_helpers.py new file mode 100644 index 0000000..7a78ba3 --- /dev/null +++ b/backend/eurydice/destination/receiver/packet_handler/s3_helpers.py @@ -0,0 +1,99 @@ +from typing import Any +from typing import List + +from minio.datatypes import Part + +import eurydice.destination.core.models as models +from eurydice.common import minio + + +def initiate_multipart_upload( + incoming_transferable: models.IncomingTransferable, +) -> str: + """ + Given a Transferable create an associated multipart upload + + Args: + incoming_transferable: the IncomingTransferable for which to initiate upload + + Returns: + The new multipart upload's ID + """ + return minio.client._create_multipart_upload( + bucket_name=incoming_transferable.s3_bucket_name, + object_name=str(incoming_transferable.id), + headers={}, + ) + + +def clean_etag(etag: str) -> str: + """Cleans the given etag string by removing unwanted characters. + + ETags are defined according to the S3 specification: + https://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html + + Args: + etag: the etag string to clean + + Returns: + The cleaned ETag. + """ + return "".join(char for char in etag if char.isalnum() or char == "-") + + +def _build_multipart_upload_dict( + transferable: models.IncomingTransferable, +) -> List[Any]: + """ + Given a Transferable, build the Parts dictionary required for completing the + multipart upload. + + Args: + transferable: IncomingTransferable for which to build multipart upload dict + + Returns: + A dictionary containing the list of all parts making up the Transferable's + multipart upload. + """ + + parts: List = [] + + for part in models.S3UploadPart.objects.filter( + incoming_transferable=transferable + ).order_by("part_number"): + parts.append(Part(part.part_number, part.etag.hex())) + + return parts + + +def complete_multipart_upload(transferable: models.IncomingTransferable) -> None: + """ + Given a Transferable, mark its associated S3 multipart upload as complete. + + Args: + transferable: IncomingTransferable to complete upload. + """ + minio.client._complete_multipart_upload( + bucket_name=transferable.s3_bucket_name, + object_name=transferable.s3_object_name, + upload_id=transferable.s3_upload_id, + parts=_build_multipart_upload_dict(transferable), + ) + + +def abort_multipart_upload(transferable: models.IncomingTransferable) -> None: + """ + Given a Transferable, abort its associated S3 multipart upload, if any. + + Args: + transferable: IncomingTransferable to abord upload. + """ + if transferable.s3_upload_id: + minio.client._abort_multipart_upload( + bucket_name=transferable.s3_bucket_name, + object_name=transferable.s3_object_name, + upload_id=transferable.s3_upload_id, + ) + + +__all__ = ("initiate_multipart_upload", "clean_etag", "complete_multipart_upload") diff --git a/backend/eurydice/destination/receiver/packet_receiver.py b/backend/eurydice/destination/receiver/packet_receiver.py new file mode 100644 index 0000000..827b559 --- /dev/null +++ b/backend/eurydice/destination/receiver/packet_receiver.py @@ -0,0 +1,164 @@ +import logging +import queue +import socketserver +import threading +from socket import socket +from types import TracebackType +from typing import Optional +from typing import Tuple +from typing import Type +from typing import Union + +from django.conf import settings + +from eurydice.common import protocol + +logger = logging.getLogger(__name__) + + +class _RequestHandler(socketserver.StreamRequestHandler): + server: "_Server" + + def handle(self) -> None: + """Put the data of each socket request in the server queue.""" + try: + self.server.queue.put( + self.rfile.read(), block=False + ) # pytype: disable=attribute-error + except queue.Full: + logger.error( + "Dropped Transferable - Receiver received data while " + "processing queue is at full capacity" + ) + else: + logger.debug("New block added to queue.") + + +class _Server(socketserver.TCPServer): + def __init__(self, receiving_queue: queue.Queue): + self.queue = receiving_queue + super().__init__( + server_address=( + settings.PACKET_RECEIVER_HOST, + settings.PACKET_RECEIVER_PORT, + ), + RequestHandlerClass=_RequestHandler, + ) + + def handle_error( + self, + request: Union[socket, Tuple[bytes, socket]], + client_address: Union[Tuple[str, int], str], + ) -> None: + """Log the exception arising when a socket request fails.""" + logger.exception( + f"Exception occurred during processing of request from {client_address}" + ) + + +class _ReceiverThread(threading.Thread): + def __init__(self, receiving_queue: queue.Queue): + self._queue = receiving_queue + self._server = _Server(self._queue) + super().__init__() + + def run(self) -> None: + with self._server as server: + server.serve_forever() + + def stop(self) -> None: + """Stop the server loop and incidentally the thread.""" + self._server.shutdown() + + +class ReceptionError(RuntimeError): + """Signal an error encountered while trying to get an OnTheWirePacket from the + PacketReceiver. + """ + + +class NothingToReceive(RuntimeError): + """No packet can be obtained from the PacketReceiver at this time.""" + + +class PacketReceiver: + """Receive serialized OnTheWirePackets using a receiver thread running a TCP server, + and deserialize the packets. + + Example: + >>> with packet_receiver.PacketReceiver() as r: + ... packet = r.receive(timeout=0.1) + + """ + + def __init__(self) -> None: + self._queue: queue.Queue = queue.Queue( + maxsize=settings.RECEIVER_BUFFER_MAX_ITEMS + ) + self._receiver_thread = _ReceiverThread(self._queue) + + def start(self) -> None: + """Start the PacketReceiver i.e. start the receiver thread. + + A PacketReceiver cannot be stopped then restarted. A new object must be created. + + """ + self._receiver_thread.start() + + def stop(self) -> None: + """Stop the PacketReceiver i.e. ask the receiver thread to stop and wait + for it to stop. + + """ + self._receiver_thread.stop() + self._receiver_thread.join() + + def receive( + self, block: bool = True, timeout: Optional[float] = None + ) -> protocol.OnTheWirePacket: + """Receive and deserialize an OnTheWirePacket. + + Args: + block: whether to block until a packet is available (if timeout=None) or at + most timeout seconds. If block is False, timeout is ignored. + timeout: how long to block (in seconds). None means block until + a packet is available. + + Returns: + The received and deserialized OnTheWirePacket. + + Raises: + NothingToReceive: if no packet is available at the end of the blocking + period. + ReceptionError: if an error is encountered while trying to deserialize + an OnTheWirePacket from received data. + + """ + try: + data = self._queue.get(block, timeout) + except queue.Empty: + raise NothingToReceive + + try: + return protocol.OnTheWirePacket.from_bytes(data) + except protocol.DeserializationError as exc: + raise ReceptionError from exc + + def __enter__(self): + self.start() + return self + + def __exit__( + self, + exctype: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + exc_traceback: Optional[TracebackType], + ) -> None: + self.stop() + + +__all__ = ( + "ReceptionError", + "NothingToReceive", + "PacketReceiver", +) diff --git a/backend/eurydice/destination/receiver/transferable_ingestion.py b/backend/eurydice/destination/receiver/transferable_ingestion.py new file mode 100644 index 0000000..d60b070 --- /dev/null +++ b/backend/eurydice/destination/receiver/transferable_ingestion.py @@ -0,0 +1,168 @@ +import hashlib +import io +import logging +from typing import NamedTuple + +from django.db import transaction + +from eurydice.common import minio +from eurydice.destination.core import models +from eurydice.destination.receiver.packet_handler import s3_helpers +from eurydice.destination.utils import rehash + +logger = logging.getLogger(__name__) + + +class PendingIngestionData(NamedTuple): + """ + Data ready to be ingested into a Transferable. + + Attributes: + data: data to be ingested into the Transferable. + sha1: cumulative SHA1 of current data and all data previously ingested into + the Transferable. + eof: boolean indicating whether or not the end of the Transferable has been + reached (if false, additional data should come later). + """ + + data: bytes + sha1: "hashlib._Hash" + eof: bool + + +def _store_single_range( + incoming_transferable: models.IncomingTransferable, + data: bytes, +) -> None: + """ + Add data to the file storage as a direct upload (no Multi-Part Upload). + """ + logger.debug("Start uploading full object to MinIO.") + minio.client.put_object( + bucket_name=incoming_transferable.s3_bucket_name, + object_name=incoming_transferable.s3_object_name, + data=io.BytesIO(data), + length=len(data), + ) + logger.debug("Full object successfully uploaded to MinIO.") + + +def _start_multipart_upload(incoming_transferable: models.IncomingTransferable) -> None: + """ + Initiate a Multi-Part Upload (without data yet). + """ + incoming_transferable.s3_upload_id = s3_helpers.initiate_multipart_upload( + incoming_transferable + ) + incoming_transferable.save(update_fields=["s3_upload_id"]) + + +def _get_next_part_number(incoming_transferable: models.IncomingTransferable) -> int: + """ + Calculate the number of the next Multi-Part Upload to perform on given Transferable. + """ + return ( + models.S3UploadPart.objects.filter( + incoming_transferable=incoming_transferable + ).count() + + 1 + ) + + +def _store_range( + incoming_transferable: models.IncomingTransferable, + data: bytes, +) -> None: + """ + Add data to an existing Multi-Part Upload. + """ + s3_part_number = _get_next_part_number(incoming_transferable) + + logger.debug("Start uploading object part to MinIO.") + # NOTE: if the next part takes more than 24 hours to arrive, the multipart upload + # will have been purged by minio + # https://github.com/minio/minio/blob/c0e79e28b25da4212467d1d7ecc767e732f384c2/cmd/fs-v1-multipart.go#L887 + part_upload_result = minio.client._upload_part( + bucket_name=incoming_transferable.s3_bucket_name, + object_name=incoming_transferable.s3_object_name, + data=data, + headers={}, + upload_id=incoming_transferable.s3_upload_id, + part_number=s3_part_number, + ) + logger.debug("Object part successfully uploaded to MinIO.") + + logger.debug("Create S3UploadPart object in database.") + models.S3UploadPart.objects.create( + etag=bytes.fromhex(part_upload_result), + incoming_transferable=incoming_transferable, + part_number=s3_part_number, + ) + + +def _update_incoming_transferable( + incoming_transferable: models.IncomingTransferable, + to_ingest: PendingIngestionData, +) -> None: + """ + Registers new data to the Transferable database entry, but does not send actual data + to the S3 file storage (this is done in above functions). + + Args: + incoming_transferable: Transferable to update. + to_ingest: PendingIngestionData to ingest into the IncomingTransferable. + """ + incoming_transferable.bytes_received += len(to_ingest.data) + incoming_transferable.rehash_intermediary = rehash.sha1_to_bytes(to_ingest.sha1) + + updated_fields = ["bytes_received", "rehash_intermediary"] + + if to_ingest.eof: + incoming_transferable.size = incoming_transferable.bytes_received + incoming_transferable.sha1 = to_ingest.sha1.digest() + incoming_transferable.mark_as_success(save=False) + updated_fields.extend(("size", "state", "finished_at", "sha1")) + + incoming_transferable.save(update_fields=updated_fields) + + +def ingest( + incoming_transferable: models.IncomingTransferable, + to_ingest: PendingIngestionData, +) -> None: + """ + Add data to the given Transferable. + + Args: + incoming_transferable: Transferable to add data to. + to_ingest: PendingIngestionData to ingest into the IncomingTransferable. + """ + if to_ingest.eof and not incoming_transferable.s3_upload_id: + with transaction.atomic(): + _update_incoming_transferable(incoming_transferable, to_ingest) + _store_single_range(incoming_transferable, to_ingest.data) + else: + if not incoming_transferable.s3_upload_id: + _start_multipart_upload(incoming_transferable) + + with transaction.atomic(): + _update_incoming_transferable(incoming_transferable, to_ingest) + + _store_range(incoming_transferable, to_ingest.data) + if to_ingest.eof: + s3_helpers.complete_multipart_upload(incoming_transferable) + + +def abort_ingestion(failed_transferable: models.IncomingTransferable) -> None: + """ + Abort a Transferable ingestion. This will mark the Transferable as ERROR, and + delete all associated data from the object storage. + + Args: + failed_transferable: Transferable that failed (its data will be deleted). + """ + s3_helpers.abort_multipart_upload(failed_transferable) + failed_transferable.mark_as_error() + + +__all__ = ["PendingIngestionData", "ingest", "abort_ingestion"] diff --git a/backend/eurydice/destination/receiver/transferable_ingestion_fs.py b/backend/eurydice/destination/receiver/transferable_ingestion_fs.py new file mode 100644 index 0000000..6d05621 --- /dev/null +++ b/backend/eurydice/destination/receiver/transferable_ingestion_fs.py @@ -0,0 +1,137 @@ +import hashlib +import logging +from math import ceil +from typing import NamedTuple + +from django.db import transaction + +from eurydice.destination.core import models +from eurydice.destination.storage import fs +from eurydice.destination.utils import rehash + +logger = logging.getLogger(__name__) + + +class PendingIngestionData(NamedTuple): + """ + Data ready to be ingested into a Transferable. + + Attributes: + data: data to be ingested into the Transferable. + sha1: cumulative SHA1 of current data and all data previously ingested into + the Transferable. + eof: boolean indicating whether or not the end of the Transferable has been + reached (if false, additional data should come later). + """ + + data: bytes + sha1: "hashlib._Hash" + eof: bool + + +def _storage_exists(incoming_transferable: models.IncomingTransferable) -> bool: + return fs.file_path(incoming_transferable).is_file() + + +def _create_storage_file(incoming_transferable: models.IncomingTransferable) -> None: + """ + Initiate a Multi-Part Upload (without data yet). + """ + file_path = fs.file_path(incoming_transferable) + logger.debug("Creating empty file on filesystem for multipart upload.") + file_path.parent.mkdir(parents=True, exist_ok=True) + with open(file_path, "wb") as f: + f.write(b"") + logger.debug("File created.") + + +def _store_range( + incoming_transferable: models.IncomingTransferable, + data: bytes, +) -> None: + """ + Add data to an existing Multi-Part Upload. + """ + file_path = fs.file_path(incoming_transferable) + logger.debug("Start writing data to filesystem.") + with open(file_path, "ab") as f: + f.write(data) + logger.debug("Data successfully written to filesystem.") + + +def _update_incoming_transferable( + incoming_transferable: models.IncomingTransferable, + to_ingest: PendingIngestionData, +) -> None: + """ + Registers new data to the Transferable database entry, but does not send actual data + to the file storage (this is done in above functions). + + Args: + incoming_transferable: Transferable to update. + to_ingest: PendingIngestionData to ingest into the IncomingTransferable. + """ + incoming_transferable.bytes_received += len(to_ingest.data) + incoming_transferable.rehash_intermediary = rehash.sha1_to_bytes(to_ingest.sha1) + + updated_fields = ["bytes_received", "rehash_intermediary"] + + if to_ingest.eof: + incoming_transferable.size = incoming_transferable.bytes_received + incoming_transferable.sha1 = to_ingest.sha1.digest() + incoming_transferable.mark_as_success(save=False) + updated_fields.extend(("size", "state", "finished_at", "sha1")) + + incoming_transferable.save(update_fields=updated_fields) + + +def ingest( + incoming_transferable: models.IncomingTransferable, + to_ingest: PendingIngestionData, +) -> None: + """ + Add data to the given Transferable. + + Args: + incoming_transferable: Transferable to add data to. + to_ingest: PendingIngestionData to ingest into the IncomingTransferable. + """ + if not _storage_exists(incoming_transferable): + _create_storage_file(incoming_transferable) + + with transaction.atomic(): + _update_incoming_transferable(incoming_transferable, to_ingest) + _store_range(incoming_transferable, to_ingest.data) + + # keep track of parts so that s3remover can clear broken ONGOING transferables + if not to_ingest.eof: + logger.debug("Create S3UploadPart object in database.") + part_number = ceil( + incoming_transferable.bytes_received // len(to_ingest.data) + ) + models.S3UploadPart.objects.create( + etag=bytes.fromhex("00" * 16), + incoming_transferable=incoming_transferable, + part_number=part_number, + ) + else: + incoming_transferable._clear_multipart_data() + + if to_ingest.eof: + # _update_incoming_transferable already handles that + pass + + +def abort_ingestion(failed_transferable: models.IncomingTransferable) -> None: + """ + Abort a Transferable ingestion. This will mark the Transferable as ERROR, and + delete all associated data from the object storage. + + Args: + failed_transferable: Transferable that failed (its data will be deleted). + """ + fs.delete(failed_transferable) + failed_transferable.mark_as_error() + + +__all__ = ["PendingIngestionData", "ingest", "abort_ingestion"] diff --git a/backend/eurydice/destination/storage/__init__.py b/backend/eurydice/destination/storage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/destination/storage/fs.py b/backend/eurydice/destination/storage/fs.py new file mode 100644 index 0000000..286a19b --- /dev/null +++ b/backend/eurydice/destination/storage/fs.py @@ -0,0 +1,24 @@ +from pathlib import Path + +from django.conf import settings + +from eurydice.destination.core import models + + +def file_path(incoming_transferable: models.IncomingTransferable) -> Path: + """ + Returns the expected file path for a given transferable. + """ + return ( + Path(settings.TRANSFERABLE_STORAGE_DIR) + / incoming_transferable.s3_bucket_name + / incoming_transferable.s3_object_name + ) + + +def delete(incoming_transferable: models.IncomingTransferable) -> None: + """ + Deletes data from the filesystem for a given transferable. + """ + path = file_path(incoming_transferable) + path.unlink(missing_ok=True) diff --git a/backend/eurydice/destination/utils/__init__.py b/backend/eurydice/destination/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/destination/utils/rehash.py b/backend/eurydice/destination/utils/rehash.py new file mode 100644 index 0000000..30fdbb0 --- /dev/null +++ b/backend/eurydice/destination/utils/rehash.py @@ -0,0 +1,78 @@ +"""Utility functions to dump and load the internal state of a hashlib.sha1 object. + +Requirements: OpenSSL >1.1.0 and Python 3.8+ + +Adapted from: + - https://github.com/kislyuk/rehash/blob/f1169d2adf150cf9f95717c5aa6945fba9137720/rehash/structs.py # noqa: E501 + - https://github.com/kislyuk/rehash/blob/f1169d2adf150cf9f95717c5aa6945fba9137720/rehash/__init__.py # noqa: E501 + +""" + +# pytype: disable=invalid-typevar # noqa: E800 + +import ctypes +import hashlib + + +# https://github.com/openssl/openssl/blob/master/crypto/evp/evp_local.h#L18-L33 +class EVP_MD_CTX(ctypes.Structure): # noqa: N801 + _fields_ = [ + ("digest", ctypes.c_void_p), + ("engine", ctypes.c_void_p), + ("flags", ctypes.c_ulong), + ("md_data", ctypes.POINTER(ctypes.c_char)), + ] + + +# https://github.com/python/cpython/blob/master/Modules/_hashopenssl.c#L68-L72 +class EVPobject(ctypes.Structure): + _fields_ = [ + ("ob_refcnt", ctypes.c_size_t), + ("ob_type", ctypes.c_void_p), + ("ctx", ctypes.POINTER(EVP_MD_CTX)), + ] + + +# The size of hashlib's internal data C buffer for a SHA-1 +SHA1_HASHLIB_BUFSIZE: int = 104 + + +# hashlib._Hash is a stub-only construct. To use it in live code, quote it. +# See https://github.com/python/typeshed/issues/2928#issuecomment-487889925 +def sha1_to_bytes(sha1_hashlib: "hashlib._Hash") -> bytes: + """Dump the internal state of a hashlib.sha1 object. + + Args: + sha1_hashlib: the hashlib object. + + Returns: + The bytes corresponding to the internal state of the hashlib object. + + """ + raw = ctypes.cast(ctypes.c_void_p(id(sha1_hashlib)), ctypes.POINTER(EVPobject)) + return raw.contents.ctx.contents.md_data[ # pytype: disable=attribute-error + :SHA1_HASHLIB_BUFSIZE + ] + + +def sha1_from_bytes(data: bytes) -> "hashlib._Hash": + """Load a hashlib.sha1 object from bytes. + + Args: + data: bytes to initialize the hashlib.sha1 object with. + + Returns: + An initialized hashlib.sha1. + + """ + sha1_hashlib = hashlib.sha1() # nosec + raw = ctypes.cast(ctypes.c_void_p(id(sha1_hashlib)), ctypes.POINTER(EVPobject)) + ctypes.memmove( + raw.contents.ctx.contents.md_data, # pytype: disable=attribute-error + data, + SHA1_HASHLIB_BUFSIZE, + ) + return sha1_hashlib + + +__all__ = ("sha1_to_bytes", "sha1_from_bytes", "SHA1_HASHLIB_BUFSIZE") diff --git a/backend/eurydice/origin/__init__.py b/backend/eurydice/origin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/origin/api/__init__.py b/backend/eurydice/origin/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/origin/api/apps.py b/backend/eurydice/origin/api/apps.py new file mode 100644 index 0000000..5cffe70 --- /dev/null +++ b/backend/eurydice/origin/api/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + name = "eurydice.origin.backoffice" + label = "eurydice_origin_backoffice" diff --git a/backend/eurydice/origin/api/docs/__init__.py b/backend/eurydice/origin/api/docs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/origin/api/docs/decorators.py b/backend/eurydice/origin/api/docs/decorators.py new file mode 100644 index 0000000..b124025 --- /dev/null +++ b/backend/eurydice/origin/api/docs/decorators.py @@ -0,0 +1,366 @@ +from datetime import datetime + +import humanfriendly +from django.conf import settings +from django.utils.translation import gettext_lazy as _ +from drf_spectacular import types +from drf_spectacular import utils as spectacular_utils +from rest_framework import exceptions as drf_exceptions +from rest_framework import status + +from eurydice.common.api import serializers as common_serializers +from eurydice.common.api.docs import custom_spectacular +from eurydice.common.api.docs import utils as docs +from eurydice.origin.api import exceptions +from eurydice.origin.api import serializers + +outgoing_transferable_example = { + "id": "00002800-0000-1000-8000-00805f9b34fb", + "created_at": "1969-12-28T14:15:22Z", + "name": "name_on_destination_side.txt", + "sha1": "31320896aedc8d3d1aaaee156be885ba0774da73", + "size": 97, + "user_provided_meta": { + "Metadata-Folder": "/home/data/", + "Metadata-Name": "name_on_destination_side.txt", + }, + "submission_succeeded": True, + "submission_succeeded_at": "1969-12-28T14:15:23Z", + "state": "ONGOING", + "progress": 43, + "bytes_transferred": 42, + "transfer_finished_at": "1969-12-28T14:16:42Z", + "transfer_speed": 17891337, + "transfer_estimated_finish_date": "1969-12-28T14:16:42Z", +} + +outgoing_transferable = spectacular_utils.extend_schema_view( + list=custom_spectacular.extend_schema( + operation_id="list-transferables", + summary=_("List outgoing transferables"), + description=_((settings.DOCS_PATH / "list-transferables.md").read_text()), + parameters=[ + spectacular_utils.OpenApiParameter( + name="created_after", + description="Minimum creation date within the result set.", + type=datetime, + location=spectacular_utils.OpenApiParameter.QUERY, + ), + spectacular_utils.OpenApiParameter( + name="created_before", + description="Maximum creation date within the result set.", + type=datetime, + location=spectacular_utils.OpenApiParameter.QUERY, + ), + spectacular_utils.OpenApiParameter( + name="name", + description=( + "The name (or a part of the name, for instance '.txt') to " + "filter on." + ), + type=str, + location=spectacular_utils.OpenApiParameter.QUERY, + ), + spectacular_utils.OpenApiParameter( + name="sha1", + description="The SHA1 to filter on, hexadecimal format.", + type=str, + location=spectacular_utils.OpenApiParameter.QUERY, + ), + spectacular_utils.OpenApiParameter( + name="submission_succeeded_after", + description="Minimum submission success date within the result set.", + type=datetime, + location=spectacular_utils.OpenApiParameter.QUERY, + ), + spectacular_utils.OpenApiParameter( + name="submission_succeeded_before", + description="Maximum submission success date within the result set.", + type=datetime, + location=spectacular_utils.OpenApiParameter.QUERY, + ), + spectacular_utils.OpenApiParameter( + name="transfer_finished_after", + description="Minimum transfer finish date within the result set.", + type=datetime, + location=spectacular_utils.OpenApiParameter.QUERY, + ), + spectacular_utils.OpenApiParameter( + name="transfer_finished_before", + description="Maximum transfer finish date within the result set.", + type=datetime, + location=spectacular_utils.OpenApiParameter.QUERY, + ), + ], + responses={ + status.HTTP_200_OK: spectacular_utils.OpenApiResponse( + response=serializers.OutgoingTransferableSerializer(many=True), + description=_("The list of transferables was successfully created."), + ), + status.HTTP_401_UNAUTHORIZED: docs.NotAuthenticatedResponse, + }, + examples=[ + spectacular_utils.OpenApiExample( + name="Transfer state", + value={ + "offset": 0, + "count": 1, + "new_items": False, + "pages": { + "previous": "ksRFmM6Mkw_DzwF=", + "current": "ksRFmM6Mkw_Dzw1=", + "next": "ksRFmM6Mkw_DzwQ=", + }, + "paginated_at": "2021-02-01 14:10:01+00:00", + "results": [outgoing_transferable_example], + }, + ) + ], + code_samples=[ + { + "lang": "bash", + "label": "cURL", + "source": (settings.DOCS_PATH / "list-transferables.sh").read_text(), + } + ], + tags=[_("Transferring files")], + ), + retrieve=custom_spectacular.extend_schema( + operation_id="check-transferable", + summary=_("Check a transfer's state"), + description=_((settings.DOCS_PATH / "check-transferable.md").read_text()), + responses={ + status.HTTP_200_OK: spectacular_utils.OpenApiResponse( + response=serializers.OutgoingTransferableSerializer, + description=_( + "Information about the transferable was successfully retrieved." + ), + ), + status.HTTP_401_UNAUTHORIZED: docs.NotAuthenticatedResponse, + status.HTTP_404_NOT_FOUND: docs.NotFoundResponse, + }, + examples=[ + spectacular_utils.OpenApiExample( + name="Transfer state", value=outgoing_transferable_example + ) + ], + code_samples=[ + { + "lang": "bash", + "label": "cURL", + "source": (settings.DOCS_PATH / "check-transferable.sh").read_text(), + } + ], + tags=[_("Transferring files")], + ), + create=custom_spectacular.extend_schema( + operation_id="create-transferable", + summary=_("Initiate a transfer"), + description=_( + (settings.DOCS_PATH / "create-transferable.md") + .read_text() + .format( + TRANSFERABLE_MAX_SIZE=humanfriendly.format_size( + settings.TRANSFERABLE_MAX_SIZE, + binary=True, + ) + ) + ), + request={"application/octet-stream": types.OpenApiTypes.BINARY}, + parameters=[ + spectacular_utils.OpenApiParameter( + name=f"{settings.METADATA_HEADER_PREFIX}Name", + type=str, + location=spectacular_utils.OpenApiParameter.HEADER, + required=False, + description=_("Optional file name."), + ), + spectacular_utils.OpenApiParameter( + name=f"{settings.METADATA_HEADER_PREFIX}*", + type=str, + location=spectacular_utils.OpenApiParameter.HEADER, + required=False, + description=_( + "Optional additional file metadata. Any header prefixed with " + f"`{settings.METADATA_HEADER_PREFIX}` will be restored on the " + f"destination." + ), + ), + ], + responses={ + status.HTTP_201_CREATED: spectacular_utils.OpenApiResponse( + description=_( + "This response is returned with the uploaded Transferable's " + "metadata when it has successfully been created." + ), + response=serializers.OutgoingTransferableSerializer, + ), + status.HTTP_401_UNAUTHORIZED: docs.NotAuthenticatedResponse, + status.HTTP_400_BAD_REQUEST: docs.create_open_api_response( + exceptions.MissingContentTypeError + ), + # workaround to be able to have multiple responses for one status code + f"{status.HTTP_400_BAD_REQUEST} ": docs.create_open_api_response( + exceptions.InconsistentContentLengthError + ), + # workaround to be able to have multiple responses for one status code + f"{status.HTTP_400_BAD_REQUEST} ": docs.create_open_api_response( + exceptions.InvalidContentLengthError + ), + f"{status.HTTP_400_BAD_REQUEST} ": docs.create_open_api_response( + exceptions.TransferableNotSuccessfullySubmittedError + ), + status.HTTP_413_REQUEST_ENTITY_TOO_LARGE: docs.create_open_api_response( + exceptions.RequestEntityTooLargeError + ), + status.HTTP_415_UNSUPPORTED_MEDIA_TYPE: docs.create_open_api_response( + drf_exceptions.UnsupportedMediaType + ), + }, + examples=[ + spectacular_utils.OpenApiExample( + name="Transfer state", value=outgoing_transferable_example + ) + ], + code_samples=[ + { + "lang": "bash", + "label": "cURL", + "source": (settings.DOCS_PATH / "create-transferable.sh").read_text(), + }, + { + "lang": "bash", + "label": "Bash Function", + "source": ( + settings.DOCS_PATH / "create-transferable-func.sh" + ).read_text(), + }, + ], + tags=[_("Transferring files")], + ), + destroy=custom_spectacular.extend_schema( + operation_id="revoke-transferable", + summary=_("Cancel a transfer"), + description=_((settings.DOCS_PATH / "revoke-transferable.md").read_text()), + responses={ + status.HTTP_204_NO_CONTENT: spectacular_utils.OpenApiResponse( + description=_("The transferable was successfully revoked."), + ), + status.HTTP_401_UNAUTHORIZED: docs.NotAuthenticatedResponse, + status.HTTP_404_NOT_FOUND: docs.NotFoundResponse, + }, + code_samples=[ + { + "lang": "bash", + "label": "cURL", + "source": (settings.DOCS_PATH / "revoke-transferable.sh").read_text(), + } + ], + tags=[_("Transferring files")], + ), +) + +user_association = spectacular_utils.extend_schema_view( + get=custom_spectacular.extend_schema( + operation_id="associate-users", + summary=_("Link two user accounts"), + description=_((settings.DOCS_PATH / "link-user-accounts.md").read_text()), + responses={ + status.HTTP_200_OK: spectacular_utils.OpenApiResponse( + description=_("An association token has successfully been created."), + response=common_serializers.AssociationTokenSerializer, + examples=[ + spectacular_utils.OpenApiExample( + _("Generate association token"), + description=_( + "The response body will respect this format (the " + "expiration date is provided for information purposes only)" + ), + value={ + "token": ( + "BIENAYME aggraver juteuse deferer AZOIQUE inavouee " + "COUQUE mixte chaton PERIPATE exaucant fourgue pastis " + "ayuthia FONDIS prostre HALLE TAVAUX" + ), + "expires_at": "2021-07-02T09:59:57Z", + }, + ), + ], + ), + status.HTTP_401_UNAUTHORIZED: docs.NotAuthenticatedResponse, + }, + code_samples=[ + { + "lang": "bash", + "label": "cURL", + "source": (settings.DOCS_PATH / "link-user-accounts.sh").read_text(), + } + ], + tags=[_("Account management")], + ) +) + +metrics = spectacular_utils.extend_schema_view( + get=custom_spectacular.extend_schema( + operation_id="check-rolling-metrics", + summary=_("Access rolling metrics"), + description=_((settings.DOCS_PATH / "check-rolling-metrics.md").read_text()), + responses={ + status.HTTP_200_OK: spectacular_utils.OpenApiResponse( + description=_("Rolling metrics have successfully been created."), + response=serializers.RollingMetricsSerializer, + examples=[ + spectacular_utils.OpenApiExample( + _("Read metrics"), + value={ + "pending_transferables": 8, + "ongoing_transferables": 3, + "recent_successes": 14, + "recent_errors": 0, + "last_packet_sent_at": "2023-09-12T16:03:57.217694+02:00", + }, + ), + ], + ), + status.HTTP_401_UNAUTHORIZED: docs.NotAuthenticatedResponse, + }, + code_samples=[ + { + "lang": "bash", + "label": "cURL", + "source": (settings.DOCS_PATH / "check-rolling-metrics.sh").read_text(), + } + ], + tags=[_("Administration")], + ) +) + +sender_status = spectacular_utils.extend_schema_view( + get=custom_spectacular.extend_schema( + operation_id="get-status", + summary=_("Get sender status"), + responses={ + status.HTTP_200_OK: spectacular_utils.OpenApiResponse( + response=serializers.StatusSerializer, + examples=[ + spectacular_utils.OpenApiExample( + _("Get sender status"), + value={ + "maintenance": False, + "last_packet_sent_at": "2023-07-24T12:39:23.320950Z", + }, + ), + ], + ), + status.HTTP_401_UNAUTHORIZED: docs.NotAuthenticatedResponse, + }, + tags=[_("Administration")], + ) +) + +__all__ = ( + "sender_status", + "metrics", + "outgoing_transferable", + "user_association", +) diff --git a/backend/eurydice/origin/api/docs/static/basic-auth.md b/backend/eurydice/origin/api/docs/static/basic-auth.md new file mode 100644 index 0000000..eb39996 --- /dev/null +++ b/backend/eurydice/origin/api/docs/static/basic-auth.md @@ -0,0 +1,35 @@ +Eurydice accepts the [Basic authentication scheme](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme). + +Simply pass your credentials as a base64 encoded username/password pair through the [`Authorization`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) header, the way basic auth works. + +With curl, you may use the `-u` flag: + +```bash +curl -u $EURYDICE_USERNAME:$EURYDICE_PASSWORD "https://$EURYDICE_ORIGIN_HOST/api/v1/transferables/" +``` + +[Unsafe requests](https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP) also require you to specify the Referer (sic) header. + +Example : +```bash +# Upload a transferable +curl -X "POST" \ + "https://$EURYDICE_ORIGIN_HOST" \ + -u $EURYDICE_USERNAME:$EURYDICE_PASSWORD \ + --request-target "/api/v1/transferables/" \ + -H "Referer: https://$EURYDICE_ORIGIN_HOST/" \ + -H "Accept: application/json" \ + -H "Content-Type: application/octet-stream" \ + -T my_file +``` + +For [safe requests](https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP) (notably GET requests) you don't need the Referer. Example: + +```bash +# List transferables +curl \ + "https://$EURYDICE_ORIGIN_HOST" \ + -u $EURYDICE_USERNAME:$EURYDICE_PASSWORD \ + --request-target "/api/v1/transferables/" \ + -H "Accept: application/json" +``` diff --git a/backend/eurydice/origin/api/docs/static/check-rolling-metrics.md b/backend/eurydice/origin/api/docs/static/check-rolling-metrics.md new file mode 100644 index 0000000..20cdab3 --- /dev/null +++ b/backend/eurydice/origin/api/docs/static/check-rolling-metrics.md @@ -0,0 +1,8 @@ +This endpoint returns information about the current state of the origin server. **Only authorized users** can access this endpoint. + +It returns two types of metrics : + +- **instant transferable counts**: instant values at query time ; +- **sliding window counts**: cumulative values within a specified time frame preceding the query. + +Sliding window metrics time frame can be configured through the `METRICS_SLIDING_WINDOW` environment variable. diff --git a/backend/eurydice/origin/api/docs/static/check-rolling-metrics.sh b/backend/eurydice/origin/api/docs/static/check-rolling-metrics.sh new file mode 100644 index 0000000..b520c86 --- /dev/null +++ b/backend/eurydice/origin/api/docs/static/check-rolling-metrics.sh @@ -0,0 +1,3 @@ +curl "https://$EURYDICE_ORIGIN_HOST/api/v1/metrics/" \ + -H "Accept: application/json" \ + -H "Authorization: Token $EURYDICE_ORIGIN_AUTHTOKEN" diff --git a/backend/eurydice/origin/api/docs/static/check-transferable.md b/backend/eurydice/origin/api/docs/static/check-transferable.md new file mode 100644 index 0000000..4abd126 --- /dev/null +++ b/backend/eurydice/origin/api/docs/static/check-transferable.md @@ -0,0 +1,5 @@ +This endpoint returns information about an outgoing transferable, including its transfer status. +If the status is still ONGOING, the progress percentage and the estimated time of arrival tell you about ongoing operations. + +You may notice that the estimated time of arrival changes from one transferable to another or even in the middle of a given transfer. +It is because it takes into account other users and their bandwidth consumption in real time. diff --git a/backend/eurydice/origin/api/docs/static/check-transferable.sh b/backend/eurydice/origin/api/docs/static/check-transferable.sh new file mode 100644 index 0000000..66113fa --- /dev/null +++ b/backend/eurydice/origin/api/docs/static/check-transferable.sh @@ -0,0 +1,4 @@ +# Replace {id} with your transferable's ID, without the braces +curl "https://$EURYDICE_ORIGIN_HOST/api/v1/transferables/{id}/" \ + -H "Accept: application/json" \ + -H "Authorization: Token $EURYDICE_ORIGIN_AUTHTOKEN" diff --git a/backend/eurydice/origin/api/docs/static/cookie-auth.md b/backend/eurydice/origin/api/docs/static/cookie-auth.md new file mode 100644 index 0000000..64824e8 --- /dev/null +++ b/backend/eurydice/origin/api/docs/static/cookie-auth.md @@ -0,0 +1,37 @@ +This is the preferred way to authenticate. + +1. Make a GET request to api/v1/user/login/ to obtain a session cookie. How to authenticate to api/v1/user/login/ depends on your reverse proxy configuration. + +For example, if it's configured to use Kerberos, you need a valid Kerberos ticket, then you can use curl's `--negotiate -u :` option. + +```bash +# This will output eurydice_sessionid and eurydice_csrftoken which you will need for the next step. +curl -c - -L --negotiate -u : "https://$EURYDICE_ORIGIN_HOST/api/v1/user/login/" -H "Accept: application/json" +``` + +2. Send eurydice_sessionid together with eurydice_csrftoken to authenticate subsequent requests. You also need to specify the Referer (sic) header. + +Example : +```bash +# Upload a transferable +curl -X "POST" \ + "https://$EURYDICE_ORIGIN_HOST" \ + --cookie "eurydice_sessionid=$SESSION_ID; eurydice_csrftoken=$CSRF_TOKEN" \ + --request-target "/api/v1/transferables/" \ + -H "Referer: https://$EURYDICE_ORIGIN_HOST/" \ + -H "X-CSRFToken: $CSRF_TOKEN" \ + -H "Accept: application/json" \ + -H "Content-Type: application/octet-stream" \ + -T my_file +``` + +For [safe requests](https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP) (notably GET requests) you don't need the Referer or the CSRF token. Example: + +```bash +# List transferables +curl \ + "https://$EURYDICE_ORIGIN_HOST" \ + --cookie "eurydice_sessionid=$SESSION_ID" \ + --request-target "/api/v1/transferables/" \ + -H "Accept: application/json" +``` diff --git a/backend/eurydice/origin/api/docs/static/cookie-auth.sh b/backend/eurydice/origin/api/docs/static/cookie-auth.sh new file mode 100644 index 0000000..ae5f20c --- /dev/null +++ b/backend/eurydice/origin/api/docs/static/cookie-auth.sh @@ -0,0 +1,3 @@ +curl -c - -L --negotiate -u : \ + "https://$EURYDICE_ORIGIN_HOST/api/v1/user/login/" \ + -H "Accept: application/json" diff --git a/backend/eurydice/origin/api/docs/static/create-transferable-func.sh b/backend/eurydice/origin/api/docs/static/create-transferable-func.sh new file mode 100644 index 0000000..d1b2d70 --- /dev/null +++ b/backend/eurydice/origin/api/docs/static/create-transferable-func.sh @@ -0,0 +1,16 @@ +function send_file { + function encodebasename { + python3 \ + -c "import sys, urllib.parse; \ + print(urllib.parse.quote(sys.argv[1]))" \ + "$(basename "$1")" + } + curl -X "POST" \ + "https://$EURYDICE_ORIGIN_HOST" \ + --request-target "/api/v1/transferables/" \ + -H "Accept: application/json" \ + -H "Authorization: Token $EURYDICE_ORIGIN_AUTHTOKEN" \ + -H "Content-Type: application/octet-stream" \ + -H "Metadata-Name: $(encodebasename "$1")" \ + -T "$1" +} && echo "Usage: send_file FILE" diff --git a/backend/eurydice/origin/api/docs/static/create-transferable.md b/backend/eurydice/origin/api/docs/static/create-transferable.md new file mode 100644 index 0000000..7633830 --- /dev/null +++ b/backend/eurydice/origin/api/docs/static/create-transferable.md @@ -0,0 +1,13 @@ +You can send a new transferable through the network diode with this endpoint by uploading its binary content in the POST requests' body. +You can submit the file together with some metadata items, as described below. + +HTTP headers starting with `Metadata-` are special when sent to this endpoint: they allow for the registration of metadata items that will be forwarded through the diode and can be retrieved on the destination side as response headers. +Among them, the header Metadata-Name is used to set the name of the file to transfer. + +⚠️ **Warning:** HTTP headers only support a restricted set of characters. +To avoid running into unexpected issues, please use `base64` to transport arbitrary data. + +⚠️ **Warning:** The maximum size allowed for a single file is {TRANSFERABLE_MAX_SIZE}. +If a POST request's body is greater than this limit, the server replies with a 413 HTTP code. + +If you want your filenames to respect the HTTP headers format, but still want to be able to read them (and display them properly in the Eurydice's web application), please use URL encoding instead of base64 (`urllib.parse.quote`/`urllib.parse.unquote` functions in Python). diff --git a/backend/eurydice/origin/api/docs/static/create-transferable.sh b/backend/eurydice/origin/api/docs/static/create-transferable.sh new file mode 100644 index 0000000..fb3b412 --- /dev/null +++ b/backend/eurydice/origin/api/docs/static/create-transferable.sh @@ -0,0 +1,9 @@ +curl -X "POST" \ + "https://$EURYDICE_ORIGIN_HOST" \ + --request-target "/api/v1/transferables/" \ + -H "Accept: application/json" \ + -H "Authorization: Token $EURYDICE_ORIGIN_AUTHTOKEN" \ + -H "Content-Type: application/octet-stream" \ + -H "Metadata-Name: name_on_destination_side.txt" \ + -H "Metadata-Folder: /home/data/" \ + -T file_to_transfer.txt diff --git a/backend/eurydice/origin/api/docs/static/link-user-accounts.md b/backend/eurydice/origin/api/docs/static/link-user-accounts.md new file mode 100644 index 0000000..4ae2a8e --- /dev/null +++ b/backend/eurydice/origin/api/docs/static/link-user-accounts.md @@ -0,0 +1,37 @@ +To transfer files using Eurydice, you first need to get a user account both on the origin and on the destination side. +To obtain one, contact your local friendly system administrators, they will create the required accounts and communicate to you the credentials. + +Once this is done, you need to associate your user account located on the origin side with the one on the destination side. +As the link between the origin and the destination networks is one way, this process must be performed manually to guarantee its success. + +Proceed as follow : + +- On the origin side, generate an association token, for instance using the cURL snippet provided for this request. + +- The response holds the association token in the form of a sequence of words, together with its expiration date: + +```json +{ + "token": "infante SOIN BARBEAU agvin MARBRES AUXERRE JABOT LECHEUR +rythme mascara FUN OESTRAUX ABJURANT sobriete APANAGE BEVATRON bufflant +GLOSER", + "expires_at": "2021-06-29T13:57:33Z" +} +``` + +- Take good note of the words of the token. + +- Then, access the Eurydice API from the destination network. + On the destination-side API, submit the association token obtained from the origin API: + +```bash +curl -X "POST" "https://$EURYDICE_DESTINATION_HOST/api/v1/user/association/" \ + -H "Authorization: Token $EURYDICE_DESTINATION_AUTHTOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "token": "infante SOIN BARBEAU agvin MARBRES AUXERRE JABOT LECHEUR rythme mascara FUN OESTRAUX ABJURANT sobriete APANAGE BEVATRON bufflant GLOSER" + }' +``` + +The server should reply with the 204 HTTP code, meaning that the user account on the destination side was successfully associated with the one on the origin side. +If you had already transferred files before the association, they should now appear on the destination side. diff --git a/backend/eurydice/origin/api/docs/static/link-user-accounts.sh b/backend/eurydice/origin/api/docs/static/link-user-accounts.sh new file mode 100644 index 0000000..2faabdc --- /dev/null +++ b/backend/eurydice/origin/api/docs/static/link-user-accounts.sh @@ -0,0 +1,3 @@ +curl "https://$EURYDICE_ORIGIN_HOST/api/v1/user/association/" \ + -H "Accept: application/json" \ + -H "Authorization: Token $EURYDICE_ORIGIN_AUTHTOKEN" diff --git a/backend/eurydice/origin/api/docs/static/list-transferables.md b/backend/eurydice/origin/api/docs/static/list-transferables.md new file mode 100644 index 0000000..56a3199 --- /dev/null +++ b/backend/eurydice/origin/api/docs/static/list-transferables.md @@ -0,0 +1,9 @@ +This endpoint lists user's outgoing transferables. +If the status is still ONGOING, the progress percentage and the estimated time of arrival tell you about ongoing operations. + +To navigate through the pages, request the previous, current and next page identifiers (found in the API's response) like so : `GET /api/v1/transferables/?page={identifier of the page you want}`. + +If new transferables are submitted while you are browsing with page identifiers, they won't be displayed in results but the new_items indicator will be set to true. +You can then request the first page again, without the page query parameter, to see them. + +N.B. While the standard way to explore transferables is to use the previous, current and next page identifiers from responses, you can also use the more advanced syntax and jump arbitrary amounts of pages via `GET /api/v1/transferables/?delta={positive or negative amount of pages}&from={any page identifier}`. diff --git a/backend/eurydice/origin/api/docs/static/list-transferables.sh b/backend/eurydice/origin/api/docs/static/list-transferables.sh new file mode 100644 index 0000000..20fa6e2 --- /dev/null +++ b/backend/eurydice/origin/api/docs/static/list-transferables.sh @@ -0,0 +1,3 @@ +curl "https://$EURYDICE_ORIGIN_HOST/api/v1/transferables/" \ + -H "Accept: application/json" \ + -H "Authorization: Token $EURYDICE_ORIGIN_AUTHTOKEN" diff --git a/backend/eurydice/origin/api/docs/static/revoke-transferable.md b/backend/eurydice/origin/api/docs/static/revoke-transferable.md new file mode 100644 index 0000000..d859be8 --- /dev/null +++ b/backend/eurydice/origin/api/docs/static/revoke-transferable.md @@ -0,0 +1,5 @@ +You can revoke an outgoing transferable waiting to be processed (*PENDING*) or currently being transferred (*ONGOING*). +Any ongoing transfer for this transferable will be interrupted, its data will be deleted from both origin and destination storage, and the transferable will be removed from the queue (giving way to awaiting transferables). + +Please note that a transferable cannot be revoked using this API endpoint if it is still being submitted, nor if it is already fully transferred. + diff --git a/backend/eurydice/origin/api/docs/static/revoke-transferable.sh b/backend/eurydice/origin/api/docs/static/revoke-transferable.sh new file mode 100644 index 0000000..bbdd8df --- /dev/null +++ b/backend/eurydice/origin/api/docs/static/revoke-transferable.sh @@ -0,0 +1,4 @@ +# Replace {id} with your transferable's ID, without the braces +curl -X "DELETE" "https://$EURYDICE_ORIGIN_HOST/api/v1/transferables/{id}/" \ + -H "Accept: application/json" \ + -H "Authorization: Token $EURYDICE_ORIGIN_AUTHTOKEN" diff --git a/backend/eurydice/origin/api/docs/static/token-auth.md b/backend/eurydice/origin/api/docs/static/token-auth.md new file mode 100644 index 0000000..1d74030 --- /dev/null +++ b/backend/eurydice/origin/api/docs/static/token-auth.md @@ -0,0 +1,20 @@ +This method of authentication is intended for non human users (scripts, software etc...) only. + +Auth tokens are typically given to service accounts, and created by administrators. If you need to access the API using an auth token, ask an administrator. + +You'll have to prefix your token in the HTTP header with the keyword `Token` like so: `Authorization: Token dcd98b7102dd2f0e8b11d0f600bfb0c093` + +Example: + +```bash +# Upload a transferable +curl -X "POST" \ + "https://$EURYDICE_ORIGIN_HOST" \ + --request-target "/api/v1/transferables/" \ + -H "Accept: application/json" \ + -H "Authorization: Token $EURYDICE_ORIGIN_AUTHTOKEN" \ + -H "Content-Type: application/octet-stream" \ + -H "Metadata-Name: name_on_destination_side.txt" \ + -H "Metadata-Folder: /home/data/" \ + -T file_to_transfer.txt +``` diff --git a/backend/eurydice/origin/api/docs/static/user-me.sh b/backend/eurydice/origin/api/docs/static/user-me.sh new file mode 100644 index 0000000..490d38d --- /dev/null +++ b/backend/eurydice/origin/api/docs/static/user-me.sh @@ -0,0 +1,3 @@ +curl "https://$EURYDICE_ORIGIN_HOST/api/v1/user/me/" \ + -H "Accept: application/json" \ + -H "Authorization: Token $EURYDICE_ORIGIN_AUTHTOKEN" diff --git a/backend/eurydice/origin/api/exceptions.py b/backend/eurydice/origin/api/exceptions.py new file mode 100644 index 0000000..bdaff10 --- /dev/null +++ b/backend/eurydice/origin/api/exceptions.py @@ -0,0 +1,84 @@ +from django.utils.translation import gettext_lazy as _ +from rest_framework import exceptions +from rest_framework import status + + +class MissingContentTypeError(exceptions.APIException): + """Exception raised when the Content-Type header is missing when it was expected.""" + + status_code = status.HTTP_400_BAD_REQUEST + default_detail = _("Content-Type header is missing") + # mypy does not recognize qualname + # https://github.com/python/mypy/issues/6473 + default_code = __qualname__ + + +class InconsistentContentLengthError(exceptions.APIException): + """Exception raised when the Content-Length does not match the + number of bytes read from the request's body. + + Args: + read: the number of bytes actually read. + expected: the expected number of bytes. + + """ + + def __init__(self, read: int, expected: int): + super().__init__( + detail=_( + "Content-Length header does not match the size of the request's body. " + f"Read {read} bytes, expected {expected}." + ) + ) + + status_code = status.HTTP_400_BAD_REQUEST + default_detail = _( + "Content-Length header does not match the size of the request's body." + ) + # mypy does not recognize qualname + # https://github.com/python/mypy/issues/6473 + default_code = __qualname__ + + +class InvalidContentLengthError(exceptions.APIException): + """Exception raised when the Content-Length header has an invalid value""" + + status_code = status.HTTP_400_BAD_REQUEST + default_detail = _("Invalid value for Content-Length header") + # mypy does not recognize qualname + # https://github.com/python/mypy/issues/6473 + default_code = __qualname__ + + +class RequestEntityTooLargeError(exceptions.APIException): + """Exception raised when the Content-Length exceeds the configured + maximum Transferable size. + """ + + status_code = status.HTTP_413_REQUEST_ENTITY_TOO_LARGE + default_detail = _( + "The uploaded Transferable is too large or the Content-Length header indicates " + "that the Transferable in the body will be too large to be saved." + ) + # mypy does not recognize qualname + # https://github.com/python/mypy/issues/6473 + default_code = __qualname__ + + +class TransferableNotSuccessfullySubmittedError(exceptions.APIException): + """Exception raised when a transferable has not been successfully submitted.""" + + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "The uploaded transferable has not been successfully submitted." + # mypy does not recognize qualname + # https://github.com/python/mypy/issues/6473 + default_code = __qualname__ + + +__all__ = ( + "MissingContentTypeError", + "InconsistentContentLengthError", + "InvalidContentLengthError", + "RequestEntityTooLargeError", + "TransferableNotSuccessfullySubmittedError", +) diff --git a/backend/eurydice/origin/api/filters.py b/backend/eurydice/origin/api/filters.py new file mode 100644 index 0000000..3ab71c6 --- /dev/null +++ b/backend/eurydice/origin/api/filters.py @@ -0,0 +1,33 @@ +from django_filters import rest_framework as filters + +from eurydice.common import enums +from eurydice.common.api import filters as common_filters +from eurydice.origin.core import models + + +class OutgoingTransferableFilter(filters.FilterSet): + """ + The set of filters for selecting OutgoingTransferables on the origin side. + """ + + created = filters.IsoDateTimeFromToRangeFilter(field_name="created_at") + + submission_succeeded = filters.IsoDateTimeFromToRangeFilter( + field_name="submission_succeeded_at" + ) + + transfer_finished = filters.IsoDateTimeFromToRangeFilter( + field_name="transfer_finished_at" + ) + + state = filters.MultipleChoiceFilter( + field_name="state", choices=enums.OutgoingTransferableState.choices + ) + + name = filters.CharFilter(field_name="name", lookup_expr="icontains") + + sha1 = common_filters.SHA1Filter(field_name="sha1") + + class Meta: + model = models.OutgoingTransferable + fields = () diff --git a/backend/eurydice/origin/api/parsers.py b/backend/eurydice/origin/api/parsers.py new file mode 100644 index 0000000..789be70 --- /dev/null +++ b/backend/eurydice/origin/api/parsers.py @@ -0,0 +1,19 @@ +from rest_framework import parsers + + +class OctetStreamParser(parsers.BaseParser): + """ + DRF parser class for handling raw octet-stream file uploads + """ + + media_type = "application/octet-stream" + + def parse( + self, stream, media_type=None, parser_context=None # noqa: ANN001 + ): # noqa: ANN201 + """ + NOTE: This is not actually used by the OutgoingTransferable creation view + because it accesses the HTTP body stream directly not through request.data + so we don't return anything + """ + pass # pragma: no cover diff --git a/backend/eurydice/origin/api/serializers.py b/backend/eurydice/origin/api/serializers.py new file mode 100644 index 0000000..a05ad51 --- /dev/null +++ b/backend/eurydice/origin/api/serializers.py @@ -0,0 +1,122 @@ +from django.conf import settings +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers as drf_serializers + +from eurydice.common import enums +from eurydice.common.api import serializers +from eurydice.origin.core import models + + +class StatusSerializer(drf_serializers.Serializer): + maintenance = drf_serializers.BooleanField( + help_text=_("Whether the sender is currently in maintenance mode"), + ) + last_packet_sent_at = drf_serializers.DateTimeField( + help_text=_("The date the last packet was sent (either data or heartbeat)") + ) + + +class RollingMetricsSerializer(drf_serializers.Serializer): + pending_transferables = drf_serializers.IntegerField( + help_text=_("The amount of transferables currently waiting to be transferred"), + min_value=0, + ) + ongoing_transferables = drf_serializers.IntegerField( + help_text=_("The amount of transferables currently being transferred"), + min_value=0, + ) + recent_successes = drf_serializers.IntegerField( + help_text=_( + "The amount of transferables successfully transferred within the " + "last few minutes" + ), + min_value=0, + ) + recent_errors = drf_serializers.IntegerField( + help_text=_( + "The amount of transferables that failed to be transferred within the " + "last few minutes" + ), + min_value=0, + ) + queue_size = drf_serializers.IntegerField( + help_text=_("The amount of bytes currently waiting to be transferred"), + min_value=0, + ) + last_packet_sent_at = drf_serializers.DateTimeField( + help_text=_("The date the last packet was sent (either data or heartbeat)"), + ) + + +class OutgoingTransferableSerializer(drf_serializers.ModelSerializer): + state = drf_serializers.ChoiceField( + # __iter__ for this enum is overridden by the str subclassing + # so we are using __members__ to iterate over enum key values + [state.value for state in enums.OutgoingTransferableState], + help_text=_("The state of this Transferable"), + ) + + progress = drf_serializers.IntegerField( + help_text=_( + "The percentage of bytes for this Transferable that have been " + "sent through the network diode" + ), + min_value=0, + max_value=100, + ) + + submission_succeeded = drf_serializers.BooleanField( + help_text=_("Whether the submission of the file by the user succeeded") + ) + + bytes_transferred = drf_serializers.IntegerField( + source="auto_bytes_transferred", + help_text=_("The amount of bytes transferred through the network diode"), + min_value=0, + max_value=settings.TRANSFERABLE_MAX_SIZE, + ) + + transfer_speed = drf_serializers.IntegerField( + help_text=_("The transfer speed through the network diode in bytes per second"), + min_value=0, + ) + + sha1 = serializers.BytesAsHexadecimalField( + # example value + default=b"7\xf0+\xcbK\xaa\x83\xeePr|\xfe\xc3n\xdf>\xfa\xe2S<", + allow_null=True, + help_text=_("SHA-1 digest for this transferable in hexadecimal form"), + ) + + transfer_finished_at = drf_serializers.DateTimeField( + help_text=_("Date at which this transferable was fully sent through the diode"), + ) + + transfer_estimated_finish_date = drf_serializers.DateTimeField( + help_text=_( + "Date at which this transferable is expected to be fully" + " sent through the diode" + ), + ) + + class Meta: + model = models.OutgoingTransferable + fields = ( + "id", + "created_at", + "name", + "sha1", + "size", + "user_provided_meta", + "submission_succeeded", + "submission_succeeded_at", + "state", + "progress", + "bytes_transferred", + "transfer_finished_at", + "transfer_speed", + "transfer_estimated_finish_date", + ) + + +__all__ = ("OutgoingTransferableSerializer", "RollingMetricsSerializer") diff --git a/backend/eurydice/origin/api/urls.py b/backend/eurydice/origin/api/urls.py new file mode 100644 index 0000000..37010d4 --- /dev/null +++ b/backend/eurydice/origin/api/urls.py @@ -0,0 +1,26 @@ +from django.urls import path +from rest_framework import routers + +import eurydice.common.api.urls +from eurydice.origin.api import views + +router = routers.DefaultRouter() +router.register( + r"transferables", + views.OutgoingTransferableViewSet, + basename="transferable", +) + +urlpatterns = ( + eurydice.common.api.urls.urlpatterns + + router.urls + + [ + path("metrics/", views.MetricsView.as_view(), name="metrics"), + path("status/", views.StatusView.as_view(), name="status"), + path( + "user/association/", + views.UserAssociationView.as_view(), + name="user-association", + ), + ] +) diff --git a/backend/eurydice/origin/api/utils/__init__.py b/backend/eurydice/origin/api/utils/__init__.py new file mode 100644 index 0000000..f3ccab7 --- /dev/null +++ b/backend/eurydice/origin/api/utils/__init__.py @@ -0,0 +1,13 @@ +""" +Miscellaneous utilities used in the origin services of Eurydice. +""" + +from .metadata_headers import extract_metadata_from_headers +from .partitioned_stream import PartitionedStream +from .partitioned_stream import StreamPartition + +__all__ = ( + "PartitionedStream", + "StreamPartition", + "extract_metadata_from_headers", +) diff --git a/backend/eurydice/origin/api/utils/metadata_headers.py b/backend/eurydice/origin/api/utils/metadata_headers.py new file mode 100644 index 0000000..1ba5ee5 --- /dev/null +++ b/backend/eurydice/origin/api/utils/metadata_headers.py @@ -0,0 +1,27 @@ +""" +Misc. utilities for handling User submitted metadata alongside +a Transferable's content in the form of HTTP Headers. +""" + +from typing import Dict + +from django.conf import settings +from django.http import request + + +def extract_metadata_from_headers(headers: request.HttpHeaders) -> Dict[str, str]: + """Extract user provided metadata in HTTP headers + + Args: + headers: HTTP header name/value mapping + + Returns: + Metadata HTTP header name/value mapping + """ + return { + header_name: header_value + for header_name, header_value in headers.items() + # Header names should be case insensitive + # https://stackoverflow.com/a/5259004 + if header_name.startswith(settings.METADATA_HEADER_PREFIX) + } diff --git a/backend/eurydice/origin/api/utils/partitioned_stream.py b/backend/eurydice/origin/api/utils/partitioned_stream.py new file mode 100644 index 0000000..4e4cd13 --- /dev/null +++ b/backend/eurydice/origin/api/utils/partitioned_stream.py @@ -0,0 +1,112 @@ +""" +StreamPartition Object along with the PartitionedStream iterable to generate them. +""" + +import io +from typing import Callable +from typing import Optional + + +class StreamPartition: + """ + A partition of `partition_size` from the given `parent_stream`. + + Args: + read: The Stream's read Callable + partition_size: The size in bytes of the StreamPartition + initial_buffer: bytes already read from the stream. + + Attributes: + bytes_read: The number of bytes read. + + """ + + def __init__( + self, + read: Callable[[int], bytes], + partition_size: int, + initial_buffer: bytes, + ): + self._initial_buffer = initial_buffer + self._read = read + self._partition_size = partition_size + self._partition_eof = False + self.bytes_read = 0 + + def read(self, chunk_size: int = -1) -> bytes: + """Read `chunk_size` bytes from the parent stream. + + Args: + chunk_size: Number of bytes to read. Defaults to None. + + Returns: + Bytes read from the parent stream. + + """ + if self._partition_eof: + return b"" + + # default to _partition_size if no chunk_size + if chunk_size < 0: + chunk_size = self._partition_size + + # smaller chunk to prevent exceeding sub_stream_size + if self.bytes_read + chunk_size >= self._partition_size: + chunk_size = self._partition_size - self.bytes_read + self._partition_eof = True + + if self._initial_buffer and chunk_size != 0: + chunk = self._initial_buffer + self._read( + chunk_size - len(self._initial_buffer) + ) + self._initial_buffer = b"" + else: + chunk = self._read(chunk_size) + + self.bytes_read += len(chunk) + + return chunk + + +class PartitionedStream: + """ + `StreamPartition` Iterable which separates the given stream + into multiple `StreamPartition` of size `partition_size`, + calling `on_read` passing it the bytes read from the stream. + + Args: + stream: The stream to read bytes from. + partition_size: The size for each StreamPartition. + on_read: Called with bytes read as argument for each read. Defaults to None. + """ + + def __init__( + self, + stream: io.BufferedIOBase, + partition_size: int, + on_read: Optional[Callable] = None, + ): + self._stream = stream + self._partition_size = partition_size + self._on_read = on_read + + def read(self, chunk_size: int) -> bytes: + """Read `chunk_size` bytes from the stream, + calls the optional callback with the read bytes as argument + + Args: + chunk_size: Number of bytes to read from stream + + Returns: + Bytes read from the stream + """ + chunk = self._stream.read(chunk_size) + + if self._on_read is not None: + self._on_read(chunk) + + return chunk + + def __iter__(self): + while byte := self.read(1): + yield StreamPartition(self.read, self._partition_size, byte) diff --git a/backend/eurydice/origin/api/views/__init__.py b/backend/eurydice/origin/api/views/__init__.py new file mode 100644 index 0000000..279b7b7 --- /dev/null +++ b/backend/eurydice/origin/api/views/__init__.py @@ -0,0 +1,11 @@ +from .metrics import MetricsView +from .outgoing_transferable import OutgoingTransferableViewSet +from .status import StatusView +from .user_association import UserAssociationView + +__all__ = ( + "MetricsView", + "OutgoingTransferableViewSet", + "UserAssociationView", + "StatusView", +) diff --git a/backend/eurydice/origin/api/views/metrics.py b/backend/eurydice/origin/api/views/metrics.py new file mode 100644 index 0000000..6b857f6 --- /dev/null +++ b/backend/eurydice/origin/api/views/metrics.py @@ -0,0 +1,73 @@ +import datetime +from typing import Dict +from typing import Optional +from typing import Union + +from django.conf import settings +from django.db.models import Count +from django.db.models import Q +from django.db.models import Sum +from django.db.models.functions import Coalesce +from django.utils import timezone +from rest_framework import generics + +from eurydice.common.api.permissions import CanViewMetrics +from eurydice.common.enums import OutgoingTransferableState +from eurydice.origin.api import serializers +from eurydice.origin.api.docs import decorators as documentation +from eurydice.origin.core import enums +from eurydice.origin.core import models + + +@documentation.metrics +class MetricsView(generics.RetrieveAPIView): + """Access metrics about transferables.""" + + serializer_class = serializers.RollingMetricsSerializer + permission_classes = [CanViewMetrics] + + def get_object(self) -> Dict[str, Union[int, Optional[datetime.datetime]]]: + """Returns rolling metrics for the view to display.""" + return { + **models.OutgoingTransferable.objects_with_state_only.values( + "state" + ).aggregate( + pending_transferables=Count( + "id", filter=Q(state=OutgoingTransferableState.PENDING) + ), + ongoing_transferables=Count( + "id", filter=Q(state=OutgoingTransferableState.ONGOING) + ), + recent_successes=Count( + "id", + filter=Q( + auto_state_updated_at__gt=timezone.now() + - datetime.timedelta(seconds=settings.METRICS_SLIDING_WINDOW), + state=OutgoingTransferableState.SUCCESS, + ), + ), + recent_errors=Count( + "id", + filter=Q( + auto_state_updated_at__gt=timezone.now() + - datetime.timedelta(seconds=settings.METRICS_SLIDING_WINDOW), + state=OutgoingTransferableState.ERROR, + ), + ), + ), + **models.TransferableRange.objects.aggregate( + queue_size=Coalesce( + Sum( + "size", + filter=Q( + transfer_state=enums.TransferableRangeTransferState.PENDING + ), + ), + 0, + ) + ), + "last_packet_sent_at": models.LastPacketSentAt.get_timestamp(), + } + + +__all__ = ("MetricsView",) diff --git a/backend/eurydice/origin/api/views/outgoing_transferable.py b/backend/eurydice/origin/api/views/outgoing_transferable.py new file mode 100644 index 0000000..0b447f8 --- /dev/null +++ b/backend/eurydice/origin/api/views/outgoing_transferable.py @@ -0,0 +1,455 @@ +import hashlib +import io +import logging +import uuid +from typing import BinaryIO +from typing import List +from typing import Optional +from typing import cast + +from django.conf import settings +from django.db import transaction +from django.db.models import query +from django.utils import decorators +from django.utils import timezone +from django_filters import rest_framework as filters +from rest_framework import exceptions as drf_exceptions +from rest_framework import mixins +from rest_framework import request as drf_request +from rest_framework import response as drf_response +from rest_framework import status +from rest_framework import throttling +from rest_framework import viewsets + +from eurydice.common import enums +from eurydice.common import minio +from eurydice.common.api import pagination +from eurydice.common.api import permissions +from eurydice.origin.api import exceptions +from eurydice.origin.api import filters as origin_filters +from eurydice.origin.api import parsers +from eurydice.origin.api import serializers +from eurydice.origin.api import utils +from eurydice.origin.api.docs import decorators as documentation +from eurydice.origin.core import models + +logger = logging.getLogger(__name__) + +_UPLOAD_FILEOBJ_MAX_CONCURRENCY = 1 +_UNKNOWN_FILEOBJ_LENGTH = -1 + +_hash_func = hashlib.sha1 + + +def _store_transferable_range( + stream_partition: utils.StreamPartition, + transferable: models.OutgoingTransferable, +) -> None: + """Upload TransferableRange's data to minio and update the transferable's size. + + Args: + stream_partition: StreamPartition to read the TransferableRange's data from + transferable: The associated OutgoingTransferable django model instance + + """ + transferable_range_id = uuid.uuid4() + s3_object_name = str(transferable_range_id) + + file_obj = cast(BinaryIO, stream_partition) + minio.client.put_object( + bucket_name=settings.MINIO_BUCKET_NAME, + object_name=s3_object_name, + data=file_obj, + length=_UNKNOWN_FILEOBJ_LENGTH, + part_size=settings.MULTIPART_PART_SIZE, + num_parallel_uploads=_UPLOAD_FILEOBJ_MAX_CONCURRENCY, + ) + + transferable_range = models.TransferableRange( + id=transferable_range_id, + outgoing_transferable=transferable, + byte_offset=transferable.bytes_received, + size=stream_partition.bytes_read, + s3_bucket_name=settings.MINIO_BUCKET_NAME, + s3_object_name=s3_object_name, + ) + + transferable.bytes_received += transferable_range.size + transferable.save(update_fields=["bytes_received"]) + transferable_range.save() + + +def _finalize_transferable( + transferable: models.OutgoingTransferable, digest: "hashlib._Hash" +) -> None: + """Mark the transferable as successfully submitted and save the hash of the file.""" + transferable.sha1 = digest.digest() + transferable.submission_succeeded_at = timezone.now() + + updated_fields = ["sha1", "submission_succeeded_at"] + + if transferable.size is None: + transferable.size = transferable.bytes_received + updated_fields.append("size") + + transferable.save(update_fields=updated_fields) + + +@transaction.atomic +def _perform_create_empty_transferable_range( + transferable: models.OutgoingTransferable, +) -> None: + """Create a transferable range of size 0 in the database and in the S3 storage.""" + empty_partition = utils.StreamPartition(lambda *args: b"", 0, b"") + _store_transferable_range(empty_partition, transferable) + _finalize_transferable(transferable, _hash_func()) + + +def _perform_create_transferable_ranges( + transferable: models.OutgoingTransferable, + stream: io.BytesIO, +) -> None: + """Create a sequence of transferable ranges from the provided stream.""" + digest = _hash_func() + + stream_partitions = iter( + utils.PartitionedStream( + stream, + settings.TRANSFERABLE_RANGE_SIZE, + digest.update, + ) + ) + + try: + partition = next(stream_partitions) + except StopIteration: + if transferable.size: + raise exceptions.InconsistentContentLengthError( + read=0, expected=transferable.size + ) + + return _perform_create_empty_transferable_range(transferable) + + while True: + # ensure the creation of a transferable range and the update of the associated + # outgoing transferable happen atomically + with transaction.atomic(): + _store_transferable_range(partition, transferable) + + if ( + transferable.size is not None + and transferable.bytes_received > transferable.size + ): + raise exceptions.InconsistentContentLengthError( + read=transferable.bytes_received, expected=transferable.size + ) + + # The _get_content_length function already checks that the contents of + # requests containing a "Content-Length" header are smaller than the max + # allowed size. According to the HTTP semantics, requests containing a + # "Transfer-Encoding" header might not expose a "Content-Length" one; + # so in these cases we can't validate the size before having integrally + # read the request's payload. As a last ressort we can at least perform + # this check while partitioning the transferable: if the current byte + # offset is beyond the maximum allowed size there's no need to read more + # data since minio won't accept such a big file. + # See: https://datatracker.ietf.org/doc/html/rfc9110#section-8.6-4 + if transferable.bytes_received > settings.TRANSFERABLE_MAX_SIZE: + raise exceptions.RequestEntityTooLargeError + + try: + partition = next(stream_partitions) + except StopIteration: + if ( + transferable.size is not None + and transferable.bytes_received != transferable.size + ): + raise exceptions.InconsistentContentLengthError( + read=transferable.bytes_received, expected=transferable.size + ) + + _finalize_transferable(transferable, digest) + + break + + +def _revoke_transferable_unexpected_exception( + transferable: models.OutgoingTransferable, +) -> None: + """Create a Transferable revocation with reason UNEXPECTED_EXCEPTION. + + Args: + transferable: the transferable that caused the issue. + + """ + models.TransferableRevocation.objects.create( + outgoing_transferable=transferable, + reason=enums.TransferableRevocationReason.UNEXPECTED_EXCEPTION, + ) + + +def _revoke_transferable_upload_size_mismatch( + transferable: models.OutgoingTransferable, +) -> None: + """ + Create a Transferable revocation with reason UPLOAD_SIZE_MISMATCH. + + Args: + transferable: the transferable that caused the issue. + + """ + models.TransferableRevocation.objects.create( + outgoing_transferable=transferable, + reason=enums.TransferableRevocationReason.UPLOAD_SIZE_MISMATCH, + ) + + +def _create_transferable_ranges( + stream: Optional[io.BytesIO], + transferable: models.OutgoingTransferable, +) -> None: + """ + Create TransferableRanges and update Transferable progressively. + + Upload TransferableRange's data to minio and save to DB. + Creates an empty TransferableRange with corresponding empty + minio object if stream is None. + + Args: + stream: the stream to read the OutgoingTransferable from + transferable: The associated OutgoingTransferable django model instance + + """ + if stream is None: + _perform_create_empty_transferable_range(transferable) + else: + _perform_create_transferable_ranges(transferable, stream) + + +def _create_transferable(request: drf_request.Request) -> models.OutgoingTransferable: + """Create many TransferableRanges from the request body and return the + saved and instantiated OutgoingTransferable django model. + + Args: + request: The DRF Request object from which to create the Transferable + + Returns: + The instantiated Django model for the saved OutgoingTransferable as well as the + instantiated Django models for the associated TransferableRanges + + Raises: + exceptions.InconsistentContentLengthError: raise this exception in case of + mismatch between the user provided Content-Length header and the size of the + transferable. + exceptions.TransferableNotSuccessfullySubmittedError: raise this exception + if the submission for this Transferable's ranges is not over at the end of + the process. + + """ + stream = _get_body_stream(request) + content_length = _get_content_length(request) + user_provided_meta = utils.extract_metadata_from_headers(request.headers) + filename = request.headers.get("metadata-name") or "" + + # the user should be authenticated + user = cast(models.User, request.user) + + logger.debug("Create OutgoingTransferable database object.") + transferable: models.OutgoingTransferable = ( + models.OutgoingTransferable.objects.create( + user_profile=user.user_profile, + user_provided_meta=user_provided_meta, + name=filename, + size=content_length, + ) + ) + + logger.debug("Start file upload to MinIO.") + try: + _create_transferable_ranges(stream, transferable) + except exceptions.InconsistentContentLengthError: + # When cancelling an upload from the frontend we start by creating a + # revocation then we abort the transfer. Thus if we reach this point + # and we encounter a USER_CANCELED revocation, this is not an error: + # the user is currently aborting right after having cancelled. + revocations = models.TransferableRevocation.objects.filter( + outgoing_transferable=transferable + ) + if (revocations.count() != 1) or ( + revocations.get().reason != enums.TransferableRevocationReason.USER_CANCELED + ): + _revoke_transferable_upload_size_mismatch(transferable) + raise + except Exception: + _revoke_transferable_unexpected_exception(transferable) + raise + logger.debug("File successfully uploaded to MinIO.") + + if not transferable.submission_succeeded: + raise exceptions.TransferableNotSuccessfullySubmittedError + + logger.debug("Save OutgoingTransferable database object.") + return models.OutgoingTransferable.objects.get(id=transferable.id) + + +def _get_body_stream(request: drf_request.Request) -> io.BytesIO: + """Get a file-like object to read the body of the HTTP request from. + + Args: + request: The DRF request object corresponding to the request. + + Returns: + A file-like object to stream read the body of the request. + + """ + if request.headers.get("Transfer-Encoding") == "chunked": + # Read the HTTP request body directly using the WSGI object provided by + # the server. This is required to handle requests using chunked transfer + # encoding, as HTTP requests with no Content-Length set are not passed + # to Django. + # + # Note: A server supporting chunked transfer encoding, such as Gunicorn, + # is required. + return request.META["wsgi.input"] # pragma: no cover + + return request.stream + + +def _get_content_length(request: drf_request.Request) -> Optional[int]: + """Attempt to check the `Content-Length` header against the configured + maximum Transferable size and return it. + + Args: + request: the request to check. + + Raises: + InvalidContentLengthError: if the `Content-Length` value cannot be parsed + as an integer. + RequestEntityTooLargeError: if the `Content-Length` value is too big. + + Returns: + Checked content length. + + """ + if request.headers.get("Transfer-Encoding") == "chunked": + # The 'Content-Length' header should be omitted when the 'Transfer-Encoding' + # header is set to 'chunked'. + # See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding#directives # noqa: E501 + logger.warning( + "Cannot retrieve the 'Content-Length' header of an HTTP request " + "using chunked transfer encoding." + ) + return None + + try: + content_length = request.headers["Content-Length"] + except KeyError: + return None + + if content_length == "": + return None + + try: + content_length = int(content_length) + except ValueError: + raise exceptions.InvalidContentLengthError + + if content_length < 0: + return None + + if content_length > settings.TRANSFERABLE_MAX_SIZE: + raise exceptions.RequestEntityTooLargeError + + return content_length + + +def _ensure_content_type_is_octet_stream(request: drf_request.Request) -> None: + """Checks that the Content-Type header value of the request is + 'application/octet-stream'. + + NOTE: This check exists because, for some reason, DRF's parsers are ignored + on the OutgoingTransferable view when using DRF's TokenAuthentication. + + Raises: + exceptions.MissingContentTypeError if there is no Content-Type header + drf_exceptions.UnsupportedMediaType if the value of the + Content-Type header is not 'application/octet-stream'. + + """ + try: + content_type = request.headers["Content-Type"] + except KeyError: + raise exceptions.MissingContentTypeError + + if content_type != "application/octet-stream": + raise drf_exceptions.UnsupportedMediaType(content_type) + + +@documentation.outgoing_transferable +# NOTE: this makes all views in the viewset use non atomic requests +# see: https://stackoverflow.com/a/49903525 and https://stackoverflow.com/a/44596892 +@decorators.method_decorator(transaction.non_atomic_requests, name="dispatch") +class OutgoingTransferableViewSet( + viewsets.GenericViewSet, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, +): + """ + Viewset to list and retrieve OutgoingTransferables which also + implements a create method for uploading file content in body. + """ + + serializer_class = serializers.OutgoingTransferableSerializer + queryset = models.OutgoingTransferable.objects.all() + pagination_class = pagination.EurydiceSessionPagination + permission_classes = [permissions.IsTransferableOwner] + filter_backends = [filters.DjangoFilterBackend] + filterset_class = origin_filters.OutgoingTransferableFilter + parser_classes = [parsers.OctetStreamParser] + throttle_scope = "create_transferable" + + def get_queryset(self) -> query.QuerySet: + """Filter queryset to only retrieve OutgoingTransferables for the current user. + + Returns: + filtered queryset + + """ + queryset = super().get_queryset() + return queryset.filter(user_profile__user__id=self.request.user.id).order_by( + "-created_at" + ) + + def get_throttles(self) -> List[throttling.BaseThrottle]: + """Instantiates and returns the list of throttles that this view uses.""" + if self.action == "create": + throttle_classes = [throttling.ScopedRateThrottle] + else: + throttle_classes = [] # No throttle for other actions + return [throttle() for throttle in throttle_classes] + + def create( + self, request: drf_request.Request, *args, **kwargs + ) -> drf_response.Response: + """Create a transferable and upload its content.""" + _ensure_content_type_is_octet_stream(request) + transferable = _create_transferable(request) + + serializer = self.get_serializer(transferable) + return drf_response.Response(serializer.data, status=status.HTTP_201_CREATED) + + def destroy( + self, request: drf_request.Request, *args, **kwargs + ) -> drf_response.Response: + """Revoke a transferable waiting to be or being transferred.""" + instance = self.get_object() + + logger.debug("Create TransferableRevocation database object.") + models.TransferableRevocation.objects.create( + outgoing_transferable=instance, + reason=enums.TransferableRevocationReason.USER_CANCELED, + ) + + logger.debug("Save TransferableRevocation database object.") + return drf_response.Response(status=status.HTTP_204_NO_CONTENT) diff --git a/backend/eurydice/origin/api/views/status.py b/backend/eurydice/origin/api/views/status.py new file mode 100644 index 0000000..d719677 --- /dev/null +++ b/backend/eurydice/origin/api/views/status.py @@ -0,0 +1,40 @@ +from datetime import datetime +from typing import Dict +from typing import Optional +from typing import Union + +from rest_framework.generics import RetrieveAPIView + +from eurydice.origin.api.docs import decorators as documentation +from eurydice.origin.api.serializers import StatusSerializer +from eurydice.origin.core import models + + +@documentation.sender_status +class StatusView(RetrieveAPIView): + """ + Retrieve sender status. + + All authenticated users can retrieve the status. + + The field "maintenance" shows whether the app is in maintenance mode. In + maintenance mode, transferables can still be submitted to the origin side but they + aren't transferred immediately to the destination side (only heartbeats are sent). + Transferables are sent after the end of the maintenance. + + The field "last_packet_sent_at" can be used to check whether the sender is + healthy: it shows when the last packet - either data or heartbeat - was sent. + (Heartbeat frequency is configured via the HEARTBEAT_SEND_EVERY environment + variable.) + """ + + serializer_class = StatusSerializer + + def get_object(self) -> Dict[str, Union[bool, Optional[datetime]]]: + """Get sender status data.""" + maintenance = models.Maintenance.is_maintenance() + last_packet_sent_at = models.LastPacketSentAt.get_timestamp() + return { + "maintenance": maintenance, + "last_packet_sent_at": last_packet_sent_at, + } diff --git a/backend/eurydice/origin/api/views/user_association.py b/backend/eurydice/origin/api/views/user_association.py new file mode 100644 index 0000000..11bb3a5 --- /dev/null +++ b/backend/eurydice/origin/api/views/user_association.py @@ -0,0 +1,23 @@ +from typing import cast + +from rest_framework import request as drf_request +from rest_framework import response as drf_response +from rest_framework import views + +from eurydice.common import association +from eurydice.common.api import serializers +from eurydice.origin.api.docs import decorators as documentation +from eurydice.origin.core import models + + +@documentation.user_association +class UserAssociationView(views.APIView): # noqa: D101 + def get( + self, request: drf_request.Request, *args, **kwargs + ) -> drf_response.Response: + """Generate an association token for a user.""" + user_profile_id = cast(models.User, request.user).user_profile.id + token = association.AssociationToken(user_profile_id) + + serializer = serializers.AssociationTokenSerializer(token) + return drf_response.Response(serializer.data) diff --git a/backend/eurydice/origin/backoffice/__init__.py b/backend/eurydice/origin/backoffice/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/origin/backoffice/admin.py b/backend/eurydice/origin/backoffice/admin.py new file mode 100644 index 0000000..cc2297a --- /dev/null +++ b/backend/eurydice/origin/backoffice/admin.py @@ -0,0 +1,168 @@ +# type: ignore + +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from django import http +from django.contrib import admin +from django.contrib.auth import admin as auth_admin +from django.db.models import query +from django.utils.translation import gettext_lazy as _ +from rest_framework.authtoken import models as token_models + +from eurydice.common import enums +from eurydice.common.backoffice import admin as common_admin +from eurydice.origin.api import serializers +from eurydice.origin.core import models + +admin.site.site_title = _("Eurydice origin admin") +admin.site.site_header = _("Eurydice origin - Administration") + +admin.site.unregister(token_models.TokenProxy) +admin.site.register(token_models.TokenProxy, common_admin.TokenAdmin) + + +class UserProfileInline(admin.StackedInline): + model = models.UserProfile + can_delete = False + + +@admin.register(models.User) +class UserAdmin(auth_admin.UserAdmin): + inlines = (UserProfileInline,) + list_display = auth_admin.UserAdmin.list_display + ("priority", "last_access") + + def get_queryset(self, request: http.HttpRequest) -> query.QuerySet: + return super().get_queryset(request).select_related("user_profile") + + def priority(self, obj: models.OutgoingTransferable) -> int: + return obj.user_profile.priority + + priority.short_description = _("Priority") + priority.admin_order_field = "user_profile__priority" + + +@admin.register(models.Maintenance) +class MaintenanceAdmin( + common_admin._DisableAddPermissionMixin, + common_admin._DisableDeletePermissionMixin, + admin.ModelAdmin, +): + fields = ("maintenance",) + + +class TransferableRangeInline(common_admin.BaseTabularInline): + model = models.TransferableRange + + +class TransferableRevocationInline(common_admin.BaseTabularInline): + model = models.TransferableRevocation + + +class StateFilter(admin.SimpleListFilter): + title = "state" + parameter_name = "state" + + def lookups( + self, request: http.HttpRequest, model_admin: Any + ) -> List[Tuple[str, str]]: + return enums.OutgoingTransferableState.choices + + def queryset( + self, request: http.HttpRequest, queryset: query.QuerySet + ) -> query.QuerySet: + if state := self.value(): + return queryset.filter(state=state) + + return queryset + + +def _get_help_texts(*field_names: str) -> Dict[str, str]: + fields = serializers.OutgoingTransferableSerializer().fields + return {name: fields[name].help_text for name in field_names} + + +@admin.register(models.OutgoingTransferable) +class OutgoingTransferableAdmin(common_admin.BaseModelAdmin): + list_display = ("id", "name", "size", "state", "progress", "user", "created_at") + list_filter = (StateFilter,) + search_fields = ("id", "name", "state", "user_profile__user__username") + + inlines = ( + TransferableRangeInline, + TransferableRevocationInline, + ) + fields = ( + "name", + "hex_sha1", + "size", + "bytes_received", + "auto_bytes_transferred", + "user", + "user_provided_meta", + "submission_succeeded", + "submission_succeeded_at", + "state", + "progress", + "created_at", + "transfer_finished_at", + "transfer_estimated_finish_date", + "transfer_speed", + "transfer_duration", + ) + help_texts = { + "hex_sha1": models.OutgoingTransferable.sha1.field.help_text, + "auto_bytes_transferred": ( + models.OutgoingTransferable.auto_bytes_transferred.field.help_text + ), + "user": _("The username of the user owning the transferable"), + "transfer_duration": _("The duration of the transfer in seconds"), + **_get_help_texts( + "submission_succeeded", + "state", + "transfer_finished_at", + "progress", + "transfer_speed", + "transfer_estimated_finish_date", + ), + } + + def get_queryset(self, request: http.HttpRequest) -> query.QuerySet: + return super().get_queryset(request).select_related("user_profile__user") + + def hex_sha1(self, obj: models.OutgoingTransferable) -> str: + return obj.sha1.hex() + + hex_sha1.short_description = _("SHA-1") + + def user(self, obj: models.OutgoingTransferable) -> str: + return obj.user_profile.user.username + + user.short_description = _("User") + user.admin_order_field = "user_profile__user__username" + + def state(self, obj: models.OutgoingTransferable) -> str: + return enums.OutgoingTransferableState(obj.state).label + + state.short_description = _("State") + state.admin_order_field = "state" + + def transfer_finished_at(self, obj: models.OutgoingTransferable) -> str: + return common_admin.format_date(obj.transfer_finished_at) + + def progress(self, obj: models.OutgoingTransferable) -> str: + return obj.progress + + progress.short_description = _("Progress") + progress.admin_order_field = "progress" + + def transfer_duration(self, obj: models.OutgoingTransferable) -> str: + return obj.transfer_duration + + def transfer_speed(self, obj: models.OutgoingTransferable) -> str: + return obj.transfer_speed + + def transfer_estimated_finish_date(self, obj: models.OutgoingTransferable) -> str: + return obj.transfer_estimated_finish_date diff --git a/backend/eurydice/origin/backoffice/apps.py b/backend/eurydice/origin/backoffice/apps.py new file mode 100644 index 0000000..8f288c2 --- /dev/null +++ b/backend/eurydice/origin/backoffice/apps.py @@ -0,0 +1,6 @@ +from django import apps + + +class OriginBackofficeConfig(apps.AppConfig): + name = "eurydice.origin.api" + label = "eurydice_origin_api" diff --git a/backend/eurydice/origin/cleaning/__init__.py b/backend/eurydice/origin/cleaning/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/origin/cleaning/dbtrimmer/__init__.py b/backend/eurydice/origin/cleaning/dbtrimmer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/origin/cleaning/dbtrimmer/__main__.py b/backend/eurydice/origin/cleaning/dbtrimmer/__main__.py new file mode 100644 index 0000000..d572677 --- /dev/null +++ b/backend/eurydice/origin/cleaning/dbtrimmer/__main__.py @@ -0,0 +1,13 @@ +if __name__ == "__main__": + import os + + import django + + os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", "eurydice.origin.config.settings.base" + ) + django.setup() + + from eurydice.origin.cleaning.dbtrimmer import dbtrimmer + + dbtrimmer.OriginDBTrimmer().start() diff --git a/backend/eurydice/origin/cleaning/dbtrimmer/dbtrimmer.py b/backend/eurydice/origin/cleaning/dbtrimmer/dbtrimmer.py new file mode 100644 index 0000000..27fdbfb --- /dev/null +++ b/backend/eurydice/origin/cleaning/dbtrimmer/dbtrimmer.py @@ -0,0 +1,106 @@ +import logging +from datetime import datetime + +from django.conf import settings +from django.utils import timezone + +from eurydice.common.cleaning import repeated_task +from eurydice.common.enums import OutgoingTransferableState +from eurydice.origin.core import models + +logging.config.dictConfig(settings.LOGGING) # type: ignore +logger = logging.getLogger(__name__) + +BULK_DELETION_SIZE = 65_536 + + +class OriginDBTrimmer(repeated_task.RepeatedTask): + """Removes old IncomingTransferables from the database. + Expired IncomingTransferables that finished + `settings.DBTRIMMER_TRIM_TRANSFERABLES_AFTER` ago will be removed. + The removal frequency is defined by `settings.DBTRIMMER_RUN_EVERY` and + `settings.DBTRIMMER_POLL_EVERY`. + """ + + def __init__(self) -> None: + super().__init__(settings.DBTRIMMER_RUN_EVERY, settings.DBTRIMMER_POLL_EVERY) + + def _ready(self) -> None: + """Logs that the OriginDBTrimmer is ready before first loop.""" + logger.info("Ready") + + def _run(self) -> None: + """Delete old transferables in a final state. + + It is safe to remove the OutgoingTransferables in a final state, + since transaction atomicity guaranties their associated S3 objects do not + exist anymore. + """ + logger.info("DBTrimmer is running") + + remove_created_before = ( + timezone.now() - settings.DBTRIMMER_TRIM_TRANSFERABLES_AFTER + ) + + finished = False + while not finished: + finished = self.trim_bulk(remove_created_before) + + logger.info("DBTrimmer finished running") + + def trim_bulk(self, remove_created_before: datetime) -> bool: + """Delete a bulk of old transferables in a final state. + + Bulk size is specified in the BULK_DELETION_SIZE variable. Since Django + retrieves TransferableRanges to delete them before deleting + OutgoingTransferables and does not clean memory as it does, it is better to + keep this value quite low to avoid excessive memory consumption. + + This function can be called in a loop as long as it returns True, meaning + transferables were successfully deleted. + """ + to_delete = models.OutgoingTransferable.objects.filter( + state__in=OutgoingTransferableState.get_final_states(), + created_at__lt=remove_created_before, + )[:BULK_DELETION_SIZE].values_list("id", flat=True) + + delete_count = len(to_delete) + if delete_count == 0: + return True + + logger.info( + f"DBTrimmer will remove {delete_count} OutgoingTransferables " + f"and all associated objects." + ) + + # Django will implicitly split the to_delete list into blocks + # of 100 IDs each, but this seems unavoidable : + # https://code.djangoproject.com/ticket/9519 + _, deletions_by_class = models.OutgoingTransferable.objects.filter( + id__in=to_delete + ).delete() + + deleted_ranges = deletions_by_class.get( + "eurydice_origin_core.TransferableRange", 0 + ) + deleted_revocations = deletions_by_class.get( + "eurydice_origin_core.TransferableRevocation", 0 + ) + deleted_transferables = deletions_by_class.get( + "eurydice_origin_core.OutgoingTransferable", 0 + ) + + logger.info( + f"DBTrimmer successfully removed {deleted_transferables} transferables, " + f"{deleted_ranges} ranges, " + f"and {deleted_revocations} revocations." + ) + + if deleted_transferables != delete_count: + logger.error( + f"DBTrimmer deleted {deleted_transferables} OutgoingTransferables, " + f"instead of the expected {delete_count}." + ) + return True + + return False diff --git a/backend/eurydice/origin/config/__init__.py b/backend/eurydice/origin/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/origin/config/settings/__init__.py b/backend/eurydice/origin/config/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/origin/config/settings/base.py b/backend/eurydice/origin/config/settings/base.py new file mode 100644 index 0000000..549425d --- /dev/null +++ b/backend/eurydice/origin/config/settings/base.py @@ -0,0 +1,209 @@ +import datetime +import pathlib + +import environ +import humanfriendly +from django.core import exceptions +from django.utils.translation import gettext_lazy as _ + +from eurydice.common.config.settings.base import ALLOWED_HOSTS +from eurydice.common.config.settings.base import AUTH_PASSWORD_VALIDATORS +from eurydice.common.config.settings.base import AUTHENTICATION_BACKENDS +from eurydice.common.config.settings.base import BASE_DIR +from eurydice.common.config.settings.base import COMMON_DOCS_PATH +from eurydice.common.config.settings.base import CSRF_COOKIE_NAME +from eurydice.common.config.settings.base import CSRF_COOKIE_SAMESITE +from eurydice.common.config.settings.base import CSRF_COOKIE_SECURE +from eurydice.common.config.settings.base import CSRF_TRUSTED_ORIGINS +from eurydice.common.config.settings.base import DATABASES +from eurydice.common.config.settings.base import DEBUG +from eurydice.common.config.settings.base import EURYDICE_CONTACT +from eurydice.common.config.settings.base import EURYDICE_CONTACT_FR +from eurydice.common.config.settings.base import EURYDICE_VERSION +from eurydice.common.config.settings.base import INSTALLED_APPS +from eurydice.common.config.settings.base import LANGUAGE_CODE +from eurydice.common.config.settings.base import LOGGING +from eurydice.common.config.settings.base import MAX_PAGE_SIZE +from eurydice.common.config.settings.base import METADATA_HEADER_PREFIX +from eurydice.common.config.settings.base import METRICS_SLIDING_WINDOW +from eurydice.common.config.settings.base import MIDDLEWARE +from eurydice.common.config.settings.base import MINIO_ACCESS_KEY +from eurydice.common.config.settings.base import MINIO_BUCKET_NAME +from eurydice.common.config.settings.base import MINIO_ENABLED +from eurydice.common.config.settings.base import MINIO_ENDPOINT +from eurydice.common.config.settings.base import MINIO_SECRET_KEY +from eurydice.common.config.settings.base import MINIO_SECURE +from eurydice.common.config.settings.base import PAGE_SIZE +from eurydice.common.config.settings.base import REMOTE_USER_HEADER +from eurydice.common.config.settings.base import ( + REMOTE_USER_HEADER_AUTHENTICATION_ENABLED, +) +from eurydice.common.config.settings.base import REST_FRAMEWORK +from eurydice.common.config.settings.base import SECRET_KEY +from eurydice.common.config.settings.base import SECURE_PROXY_SSL_HEADER +from eurydice.common.config.settings.base import SESSION_COOKIE_AGE +from eurydice.common.config.settings.base import SESSION_COOKIE_NAME +from eurydice.common.config.settings.base import SESSION_COOKIE_SAMESITE +from eurydice.common.config.settings.base import SESSION_COOKIE_SECURE +from eurydice.common.config.settings.base import SPECTACULAR_SETTINGS +from eurydice.common.config.settings.base import STATIC_ROOT +from eurydice.common.config.settings.base import STATIC_URL +from eurydice.common.config.settings.base import TEMPLATES +from eurydice.common.config.settings.base import TIME_ZONE +from eurydice.common.config.settings.base import TRANSFERABLE_MAX_SIZE +from eurydice.common.config.settings.base import TRANSFERABLE_STORAGE_DIR +from eurydice.common.config.settings.base import UI_BADGE_COLOR +from eurydice.common.config.settings.base import UI_BADGE_CONTENT +from eurydice.common.config.settings.base import USE_I18N +from eurydice.common.config.settings.base import USE_TZ +from eurydice.common.config.settings.base import USER_ASSOCIATION_TOKEN_EXPIRES_AFTER +from eurydice.common.config.settings.base import USER_ASSOCIATION_TOKEN_SECRET_KEY + +env = environ.Env( + TRANSFERABLE_RANGE_SIZE=(str, "500MB"), + MULTIPART_PART_SIZE=(int, "5MiB"), # minio minimum + TRANSFERABLE_HISTORY_DURATION=(str, "5h"), + TRANSFERABLE_HISTORY_SEND_EVERY=(str, "5min"), + PACKET_SENDER_QUEUE_SIZE=(int, 1), + MAX_TRANSFERABLES_PER_PACKET=(int, 800), + HEARTBEAT_SEND_EVERY=(str, "2min"), + SENDER_POLL_DATABASE_EVERY=(str, "0.1s"), + SENDER_RANGE_FILLER_CLASS=(str, "UserRotatingTransferableRangeFiller"), + LIDIS_HOST=(str, None), + LIDIS_PORT=(int, None), + DBTRIMMER_TRIM_TRANSFERABLES_AFTER=(str, "1day"), + DBTRIMMER_RUN_EVERY=(str, "6h"), + DBTRIMMER_POLL_EVERY=(str, "200ms"), + MINIO_EXPIRATION_DAYS=(int, 8), +) + +DOCS_PATH = pathlib.Path(BASE_DIR) / "origin" / "api" / "docs" / "static" + +INSTALLED_APPS += [ + "eurydice.origin.core.apps.CoreConfig", + "eurydice.origin.backoffice.apps.OriginBackofficeConfig", + "eurydice.origin.api.apps.ApiConfig", +] + +ROOT_URLCONF = "eurydice.origin.config.urls" + +WSGI_APPLICATION = "eurydice.origin.config.wsgi.application" + +AUTH_USER_MODEL = "eurydice_origin_core.User" + +# Ideally this would be adjusted per view, but that is not possible +# https://code.djangoproject.com/ticket/32307 +# https://docs.djangoproject.com/fr/3.2/ref/settings/#data-upload-max-memory-size +DATA_UPLOAD_MAX_MEMORY_SIZE = TRANSFERABLE_MAX_SIZE + +# drf-spectacular +# https://drf-spectacular.readthedocs.io/ + +SPECTACULAR_SETTINGS["TITLE"] = _("Eurydice origin API") + + +# Document remote user authentication only if it is enabled +if REMOTE_USER_HEADER_AUTHENTICATION_ENABLED: + SPECTACULAR_SETTINGS["APPEND_COMPONENTS"]["securitySchemes"]["cookieAuth"] = { + "type": "apiKey", + "in": "cookie", + "name": SESSION_COOKIE_NAME, + "description": _((DOCS_PATH / "cookie-auth.md").read_text()), + } +else: + SPECTACULAR_SETTINGS["APPEND_COMPONENTS"]["securitySchemes"]["basicAuth"] = { + "type": "http", + "in": "header", + "scheme": "basic", + "description": _((DOCS_PATH / "basic-auth.md").read_text()), + } + +SPECTACULAR_SETTINGS["APPEND_COMPONENTS"]["securitySchemes"]["tokenAuth"] = { + "type": "apiKey", + "in": "header", + "name": "Authorization", + "description": _((DOCS_PATH / "token-auth.md").read_text()), +} + +# S3 Client + +# The minio client uploads TransferableRanges in a single threaded multi-part upload +# This determines the size of each minio uploaded part +MULTIPART_PART_SIZE = humanfriendly.parse_size(env("MULTIPART_PART_SIZE"), binary=False) + +# Multipart uploads part sizes cannot be smaller than 5MiB in minio +if MULTIPART_PART_SIZE < humanfriendly.parse_size("5MiB"): + raise exceptions.ImproperlyConfigured( + f"MULTIPART_PART_SIZE must not be smaller than 5MiB" + f"(currently set to {humanfriendly.format_size(MULTIPART_PART_SIZE)})" + ) + + +# Eurydice + +# The size of the TransferableRanges in bytes +# i.e. the size of the file chunks transferred over the wire. +TRANSFERABLE_RANGE_SIZE = humanfriendly.parse_size( + env("TRANSFERABLE_RANGE_SIZE"), binary=False +) + +# Objects for multipart uploads cannot be smaller than 5MiB in minio +if TRANSFERABLE_RANGE_SIZE < humanfriendly.parse_size("5MiB"): + raise exceptions.ImproperlyConfigured( + f"TRANSFERABLE_RANGE_SIZE must not be smaller than 5MiB" + f"(currently set to {humanfriendly.format_size(TRANSFERABLE_RANGE_SIZE)})" + ) + + +# The time range of the history in seconds. +TRANSFERABLE_HISTORY_DURATION = humanfriendly.parse_timespan( + env("TRANSFERABLE_HISTORY_DURATION") +) + +# The sending frequency of the history in seconds. +TRANSFERABLE_HISTORY_SEND_EVERY = humanfriendly.parse_timespan( + env("TRANSFERABLE_HISTORY_SEND_EVERY") +) + +# Time to wait after having generated all packets +SENDER_POLL_DATABASE_EVERY = humanfriendly.parse_timespan( + env("SENDER_POLL_DATABASE_EVERY") +) + +# The class used for implementing TransferableFiller in OnTheWirePacket generator. +SENDER_RANGE_FILLER_CLASS = env("SENDER_RANGE_FILLER_CLASS") + +# The sending frequency of an empty heartbeat packet in seconds (if there is no data to send). +HEARTBEAT_SEND_EVERY = humanfriendly.parse_timespan(env("HEARTBEAT_SEND_EVERY")) + +# How many packets can be waiting for being sent by the PacketSender. +PACKET_SENDER_QUEUE_SIZE = env("PACKET_SENDER_QUEUE_SIZE") + +# How many transferables can coexist in an OnTheWirePacket. Overestimating this value +# can slightly slow down the sender, underestimating this value can disadvantage +# users who send large quantities of small files. +MAX_TRANSFERABLES_PER_PACKET = env("MAX_TRANSFERABLES_PER_PACKET") + +# Lidi sender service host. +LIDIS_HOST = env.str("LIDIS_HOST") + +# Lidi sender service port. +LIDIS_PORT = env.int("LIDIS_PORT") + + +# The duration after which transferables are deleted from the database. +DBTRIMMER_TRIM_TRANSFERABLES_AFTER = datetime.timedelta( + seconds=humanfriendly.parse_timespan(env("DBTRIMMER_TRIM_TRANSFERABLES_AFTER")) +) + +# How often the dbtrimmer is run. +DBTRIMMER_RUN_EVERY = datetime.timedelta( + seconds=humanfriendly.parse_timespan(env("DBTRIMMER_RUN_EVERY")) +) + +# How often the dbtrimmer polls for SIGINT. +DBTRIMMER_POLL_EVERY = datetime.timedelta( + seconds=humanfriendly.parse_timespan(env("DBTRIMMER_POLL_EVERY")) +) + +MINIO_EXPIRATION_DAYS = env("MINIO_EXPIRATION_DAYS") diff --git a/backend/eurydice/origin/config/settings/dev.py b/backend/eurydice/origin/config/settings/dev.py new file mode 100644 index 0000000..d99ac02 --- /dev/null +++ b/backend/eurydice/origin/config/settings/dev.py @@ -0,0 +1,4 @@ +from eurydice.origin.config.settings.base import * # isort:skip + +from eurydice.common.config.settings.dev import DEBUG +from eurydice.common.config.settings.dev import FAKER_SEED diff --git a/backend/eurydice/origin/config/settings/test.py b/backend/eurydice/origin/config/settings/test.py new file mode 100644 index 0000000..cf0961a --- /dev/null +++ b/backend/eurydice/origin/config/settings/test.py @@ -0,0 +1,8 @@ +import os + +os.environ.setdefault("LIDIS_HOST", "127.0.0.1") +os.environ.setdefault("LIDIS_PORT", "1") + +from eurydice.origin.config.settings.base import * + +from eurydice.common.config.settings.test import * # isort:skip diff --git a/backend/eurydice/origin/config/urls.py b/backend/eurydice/origin/config/urls.py new file mode 100644 index 0000000..152d9a5 --- /dev/null +++ b/backend/eurydice/origin/config/urls.py @@ -0,0 +1,16 @@ +from django.urls import include +from django.urls import path + +# use DRF error views +# https://www.django-rest-framework.org/api-guide/exceptions/#generic-error-views +from eurydice.common.api.urls import handler400 # noqa: F401 +from eurydice.common.api.urls import handler500 # noqa: F401 +from eurydice.common.backoffice import urls as backoffice_urls +from eurydice.common.redoc import urls as redoc_urls +from eurydice.origin.api import urls as api_urls + +urlpatterns = [ + path("admin/", include(backoffice_urls)), + path("api/v1/", include(api_urls)), + path("api/docs/", include(redoc_urls)), +] diff --git a/backend/eurydice/origin/config/wsgi.py b/backend/eurydice/origin/config/wsgi.py new file mode 100644 index 0000000..973e438 --- /dev/null +++ b/backend/eurydice/origin/config/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for eurydice project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "eurydice.origin.config.settings.base") + +application = get_wsgi_application() diff --git a/backend/eurydice/origin/core/__init__.py b/backend/eurydice/origin/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/origin/core/apps.py b/backend/eurydice/origin/core/apps.py new file mode 100644 index 0000000..0cf436b --- /dev/null +++ b/backend/eurydice/origin/core/apps.py @@ -0,0 +1,23 @@ +from django import apps +from django.conf import settings +from django.db import models +from django.db.models import functions + + +class CoreConfig(apps.AppConfig): + name = "eurydice.origin.core" + label = "eurydice_origin_core" + verbose_name = "Eurydice" + + def ready(self) -> None: + if settings.MINIO_ENABLED: + from eurydice.common.utils import s3 as s3_utils + + s3_utils.create_bucket_if_does_not_exist() + + # Import django DB signals to register them + import eurydice.origin.core.signals # noqa: F401 + + # Register lookups + models.CharField.register_lookup(functions.Length) + models.BinaryField.register_lookup(functions.Length) diff --git a/backend/eurydice/origin/core/enums.py b/backend/eurydice/origin/core/enums.py new file mode 100644 index 0000000..99d0959 --- /dev/null +++ b/backend/eurydice/origin/core/enums.py @@ -0,0 +1,22 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class TransferableRangeTransferState(models.TextChoices): + """The set of all possible transfer states for a TransferableRange.""" + + PENDING = "PENDING", _("Pending") + TRANSFERRED = "TRANSFERRED", _("Transferred") + CANCELED = "CANCELED", _("Canceled") + ERROR = "ERROR", _("Error") + + +class TransferableRevocationTransferState(models.TextChoices): + """The set of all possible transfer states for a TransferableRevocation.""" + + PENDING = "PENDING", _("Pending") + TRANSFERRED = "TRANSFERRED", _("Transferred") + ERROR = "ERROR", _("Error") + + +__all__ = ("TransferableRangeTransferState", "TransferableRevocationTransferState") diff --git a/backend/eurydice/origin/core/management/__init__.py b/backend/eurydice/origin/core/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/origin/core/management/commands/__init__.py b/backend/eurydice/origin/core/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/origin/core/management/commands/populate_db.py b/backend/eurydice/origin/core/management/commands/populate_db.py new file mode 100644 index 0000000..a8569b2 --- /dev/null +++ b/backend/eurydice/origin/core/management/commands/populate_db.py @@ -0,0 +1,52 @@ +from typing import Any + +import factory +from django.core.management import base +from django.db import transaction + +from tests.origin.integration import factory as origin_factory + + +class Command(base.BaseCommand): # noqa: D101 + help = "Populate the database with data resembling production" # noqa: VNE003 + + def add_arguments(self, parser: base.CommandParser) -> None: # noqa: D102 + parser.add_argument( + "--users", + type=int, + default=50, + help="Number of users to create.", + ) + parser.add_argument( + "--outgoing-transferables", + type=int, + default=300, + help="Number of OutgoingTransferables to create.", + ) + parser.add_argument( + "--transferable-ranges", + type=int, + default=50000, + help="Number of TransferableRanges to create.", + ) + + def handle(self, *args: Any, **options: str) -> None: + """ + Generate and populate database with data in a single query. + """ + with transaction.atomic(): + user_profiles = origin_factory.UserProfileFactory.create_batch( + options["users"] + ) + + outgoing_transferables = ( + origin_factory.OutgoingTransferableFactory.create_batch( + options["outgoing_transferables"], + user_profile=factory.Iterator(user_profiles), + ) + ) + + origin_factory.TransferableRangeFactory.create_batch( + options["transferable_ranges"], + outgoing_transferable=factory.Iterator(outgoing_transferables), + ) diff --git a/backend/eurydice/origin/core/management/commands/send_history.py b/backend/eurydice/origin/core/management/commands/send_history.py new file mode 100644 index 0000000..771a8ef --- /dev/null +++ b/backend/eurydice/origin/core/management/commands/send_history.py @@ -0,0 +1,68 @@ +import logging +import time +from typing import Any + +import humanfriendly +from django.conf import settings +from django.core.management import base + +import eurydice.common.protocol as protocol +import eurydice.origin.sender.packet_generator.fillers as fillers +import eurydice.origin.sender.utils as sender_utils +from eurydice.origin.sender import packet_sender + +logging.config.dictConfig(settings.LOGGING) # type: ignore +logger = logging.getLogger(__name__) + + +def _print_and_log(log: str, level: int = logging.INFO) -> None: + print(log) + logger.log(level, log) + + +class Command(base.BaseCommand): # noqa: D101 + help = "Force-send a history of given duration. Bypasses Maintenance mode. Only sends history, does not send Transferable ranges or revocations." # noqa: VNE003 E501 + + def add_arguments(self, parser: base.CommandParser) -> None: # noqa: D102 + parser.add_argument( + "--duration", + type=str, + default="7d", + help="Duration of history, in humanfriendly time format (15min, 6h, 7d...)", + ) + + def handle(self, *args: Any, **options: str) -> None: + """ + Sends an OTWPacket using only the History filler. + """ + settings.TRANSFERABLE_HISTORY_DURATION = humanfriendly.parse_timespan( + options["duration"] + ) + + sender_utils.check_configuration() + + _print_and_log("Preparing sender...", logging.DEBUG) + with packet_sender.PacketSender() as sender: + _print_and_log("Ready to send OnTheWirePackets") + + _print_and_log("Generating history packet...", logging.DEBUG) + start = time.perf_counter() + + # manually use OngoingHistoryFiller to bypass maintenance mode + packet = protocol.OnTheWirePacket() # type: ignore + fillers.OngoingHistoryFiller().fill(packet) + + end = time.perf_counter() + elapsed_time = end - start + _print_and_log(f"Generated packet in {elapsed_time} seconds.") + + _print_and_log("Sending packet...", logging.DEBUG) + start = time.perf_counter() + + sender.send(packet) + + end = time.perf_counter() + elapsed_time = end - start + _print_and_log(f"Packet sent in {elapsed_time} seconds.") + + _print_and_log("Done.") diff --git a/backend/eurydice/origin/core/migrations/0001_squashed_0023_make_auto_fields_non_editable.py b/backend/eurydice/origin/core/migrations/0001_squashed_0023_make_auto_fields_non_editable.py new file mode 100644 index 0000000..7b4415a --- /dev/null +++ b/backend/eurydice/origin/core/migrations/0001_squashed_0023_make_auto_fields_non_editable.py @@ -0,0 +1,1186 @@ +# Generated by Django 3.2.17 on 2023-06-05 09:22 + +import uuid +from pathlib import Path + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.core.validators +import django.db.models.deletion +import django.db.models.expressions +import django.utils.timezone +from django.conf import settings +from django.db import migrations +from django.db import models + +import eurydice.common.models.fields + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "abstract": False, + }, + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name="OutgoingTransferable", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "name", + eurydice.common.models.fields.TransferableNameField( + default="", + help_text="The name of the file corresponding to the Transferable", + max_length=255, + validators=[django.core.validators.MinLengthValidator(1)], + verbose_name="Name", + ), + ), + ( + "sha1", + eurydice.common.models.fields.SHA1Field( + default=None, + help_text="The SHA-1 digest of the file corresponding to the Transferable", + max_length=20, + null=True, + validators=[django.core.validators.MinLengthValidator(20)], + verbose_name="SHA-1", + ), + ), + ( + "size", + eurydice.common.models.fields.TransferableSizeField( + default=0, + help_text="The size in bytes of the file corresponding to the Transferable", + validators=[ + django.core.validators.MaxValueValidator(5497558138880) + ], + verbose_name="Size in bytes", + ), + ), + ( + "user_provided_meta", + eurydice.common.models.fields.UserProvidedMetaField( + default=dict, + help_text="The metadata provided by the user on file submission", + verbose_name="User provided metadata", + ), + ), + ( + "state", + models.CharField( + choices=[ + ("PENDING", "Pending"), + ("ONGOING", "Ongoing"), + ("SUCCESS", "Success"), + ("ERROR", "Error"), + ], + default="PENDING", + help_text="The state of the OutgoingTransferable", + max_length=7, + verbose_name="State", + ), + ), + ( + "started_at", + models.DateTimeField( + help_text="A timestamp indicating the start of the transfer of the Transferable", + null=True, + verbose_name="Transfer start date", + ), + ), + ( + "finished_at", + models.DateTimeField( + help_text="A timestamp indicating the end of the transfer of the Transferable", + null=True, + verbose_name="Transfer finish date", + ), + ), + ], + options={ + "db_table": "eurydice_outgoing_transferables", + }, + ), + migrations.CreateModel( + name="UserProfile", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "priority", + models.PositiveSmallIntegerField( + default=0, + help_text="The priority level of the related user for transmitting files", + verbose_name="Priority", + ), + ), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="user_profile", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "db_table": "eurydice_user_profiles", + }, + ), + migrations.CreateModel( + name="TransferableRange", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "byte_offset", + models.PositiveBigIntegerField( + help_text="The start position of this range in the associated Transferable", + validators=[ + django.core.validators.MaxValueValidator(5497558138880) + ], + verbose_name="Byte offset", + ), + ), + ( + "size", + models.PositiveIntegerField( + help_text="The size in bytes of this TransferableRange", + validators=[ + django.core.validators.MaxValueValidator(500000000) + ], + verbose_name="Size in bytes", + ), + ), + ( + "s3_bucket_name", + eurydice.common.models.fields.S3BucketNameField( + help_text="The name of the S3 bucket containing the file chunk corresponding to this TransferableRange", + max_length=63, + validators=[django.core.validators.MinLengthValidator(3)], + verbose_name="S3 bucket name", + ), + ), + ( + "s3_object_name", + eurydice.common.models.fields.S3ObjectNameField( + help_text="The name of the S3 object holding the data corresponding to this TransferableRange", + max_length=255, + validators=[django.core.validators.MinLengthValidator(1)], + verbose_name="S3 object name", + ), + ), + ( + "outgoing_transferable", + models.ForeignKey( + on_delete=django.db.models.deletion.RESTRICT, + related_name="transferable_ranges", + to="eurydice_origin_core.outgoingtransferable", + ), + ), + ], + options={ + "db_table": "eurydice_transferable_ranges", + }, + ), + migrations.AddField( + model_name="outgoingtransferable", + name="user_profile", + field=models.ForeignKey( + help_text="The profile of the user owning the Transferable", + on_delete=django.db.models.deletion.RESTRICT, + to="eurydice_origin_core.userprofile", + verbose_name="User profile", + ), + ), + migrations.AddConstraint( + model_name="transferablerange", + constraint=models.UniqueConstraint( + fields=("s3_bucket_name", "s3_object_name"), + name="eurydice_origin_core_transferablerange_s3_bucket_name_s3_object_name", + ), + ), + migrations.AddConstraint( + model_name="outgoingtransferable", + constraint=models.CheckConstraint( + check=models.Q( + models.Q(("started_at__isnull", True), ("state", "PENDING")), + models.Q( + ("started_at__isnull", False), + ("state__in", ("ONGOING", "SUCCESS", "ERROR")), + ), + _connector="OR", + ), + name="eurydice_origin_core_outgoingtransferable_started_at_state", + ), + ), + migrations.AddConstraint( + model_name="outgoingtransferable", + constraint=models.CheckConstraint( + check=models.Q( + models.Q( + ("finished_at__isnull", True), + ("state__in", ("PENDING", "ONGOING")), + ), + models.Q( + ("finished_at__isnull", False), + ("state__in", ("ERROR", "SUCCESS")), + ), + _connector="OR", + ), + name="eurydice_origin_core_outgoingtransferable_finished_at_state", + ), + ), + migrations.AddConstraint( + model_name="outgoingtransferable", + constraint=models.CheckConstraint( + check=models.Q( + ("started_at__gte", django.db.models.expressions.F("created_at")) + ), + name="eurydice_origin_core_outgoingtransferable_started_at_created_at", + ), + ), + migrations.AddConstraint( + model_name="outgoingtransferable", + constraint=models.CheckConstraint( + check=models.Q( + ("finished_at__gte", django.db.models.expressions.F("started_at")) + ), + name="eurydice_origin_core_outgoingtransferable_finished_at_started_at", + ), + ), + migrations.CreateModel( + name="TransferableRevocation", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "reason", + models.CharField( + choices=[ + ("USER_CANCELED", "Canceled by the user"), + ( + "CONNECTION_LOST", + "Connection lost during user submission", + ), + ( + "OBJECT_STORAGE_FULL", + "No more space on API object storage", + ), + ( + "UNEXPECTED_EXCEPTION", + "Unexpected error occurred while handling Transferable", + ), + ], + help_text="The reason for the OutgoingTransferable's revocation", + max_length=20, + verbose_name="Revocation reason", + ), + ), + ( + "transfer_state", + models.CharField( + choices=[ + ("PENDING", "Pending"), + ("ONGOING", "Ongoing"), + ("TRANSFERRED", "Transferred"), + ("ERROR", "Error"), + ], + help_text="The reason for the OutgoingTransferable's revocation", + max_length=11, + verbose_name="State of the Revocation", + ), + ), + ], + options={ + "db_table": "eurydice_transferable_revocations", + }, + ), + migrations.RemoveConstraint( + model_name="outgoingtransferable", + name="eurydice_origin_core_outgoingtransferable_started_at_state", + ), + migrations.RemoveConstraint( + model_name="outgoingtransferable", + name="eurydice_origin_core_outgoingtransferable_finished_at_state", + ), + migrations.RemoveConstraint( + model_name="outgoingtransferable", + name="eurydice_origin_core_outgoingtransferable_started_at_created_at", + ), + migrations.RemoveConstraint( + model_name="outgoingtransferable", + name="eurydice_origin_core_outgoingtransferable_finished_at_started_at", + ), + migrations.RemoveField( + model_name="outgoingtransferable", + name="finished_at", + ), + migrations.RemoveField( + model_name="outgoingtransferable", + name="started_at", + ), + migrations.RemoveField( + model_name="outgoingtransferable", + name="state", + ), + migrations.AddField( + model_name="outgoingtransferable", + name="submission_success", + field=models.DateTimeField( + help_text="A timestamp indicating the date at which the OutgoingTransferable's submission was successful", + null=True, + verbose_name="OutgoingTransferable's submission's success date", + ), + ), + migrations.AddField( + model_name="transferablerange", + name="finished_at", + field=models.DateTimeField( + help_text="A timestamp indicating the end of the transfer of the Transferable", + null=True, + verbose_name="Transfer finish date", + ), + ), + migrations.AddField( + model_name="transferablerange", + name="started_at", + field=models.DateTimeField( + help_text="A timestamp indicating the start of the transfer of the Transferable", + null=True, + verbose_name="Transfer start date", + ), + ), + migrations.AddField( + model_name="transferablerange", + name="transfer_state", + field=models.CharField( + choices=[ + ("PENDING", "Pending"), + ("ONGOING", "Ongoing"), + ("TRANSFERRED", "Transferred"), + ("ERROR", "Error"), + ], + default="PENDING", + help_text="The state of the transfer for this OutgoingTransferable", + max_length=11, + verbose_name="Transfer state", + ), + ), + migrations.AddConstraint( + model_name="transferablerange", + constraint=models.CheckConstraint( + check=models.Q( + models.Q( + ("started_at__isnull", True), ("transfer_state", "PENDING") + ), + models.Q( + ("started_at__isnull", False), + ("transfer_state__in", ("ONGOING", "TRANSFERRED", "ERROR")), + ), + _connector="OR", + ), + name="eurydice_origin_core_transferablerange_started_at_transfer_state", + ), + ), + migrations.AddConstraint( + model_name="transferablerange", + constraint=models.CheckConstraint( + check=models.Q( + models.Q( + ("finished_at__isnull", True), + ("transfer_state__in", ("PENDING", "ONGOING")), + ), + models.Q( + ("finished_at__isnull", False), + ("transfer_state__in", ("ERROR", "TRANSFERRED")), + ), + _connector="OR", + ), + name="eurydice_origin_core_transferablerange_finished_at_transfer_state", + ), + ), + migrations.AddConstraint( + model_name="transferablerange", + constraint=models.CheckConstraint( + check=models.Q( + ("started_at__gte", django.db.models.expressions.F("created_at")) + ), + name="eurydice_origin_core_transferablerange_started_at_created_at", + ), + ), + migrations.AddConstraint( + model_name="transferablerange", + constraint=models.CheckConstraint( + check=models.Q( + ("finished_at__gte", django.db.models.expressions.F("started_at")) + ), + name="eurydice_origin_core_transferablerange_finished_at_started_at", + ), + ), + migrations.AlterField( + model_name="transferablerevocation", + name="reason", + field=models.CharField( + choices=[ + ("USER_CANCELED", "Canceled by the user"), + ( + "UPLOAD_SIZE_MISMATCH", + "The size of the uploaded Transferable did not match the size given in the Content-Length header.", + ), + ("OBJECT_STORAGE_FULL", "No more space on API object storage"), + ( + "UNEXPECTED_EXCEPTION", + "Unexpected error occurred while handling Transferable", + ), + ], + help_text="The reason for the OutgoingTransferable's revocation", + max_length=20, + verbose_name="Revocation reason", + ), + ), + migrations.AlterField( + model_name="transferablerevocation", + name="transfer_state", + field=models.CharField( + choices=[ + ("PENDING", "Pending"), + ("ONGOING", "Ongoing"), + ("TRANSFERRED", "Transferred"), + ("ERROR", "Error"), + ], + default="PENDING", + help_text="The reason for the OutgoingTransferable's revocation", + max_length=11, + verbose_name="State of the Revocation", + ), + ), + migrations.AddField( + model_name="transferablerevocation", + name="outgoing_transferable", + field=models.OneToOneField( + on_delete=django.db.models.deletion.RESTRICT, + related_name="revocation", + to="eurydice_origin_core.outgoingtransferable", + ), + ), + migrations.RemoveConstraint( + model_name="transferablerange", + name="eurydice_origin_core_transferablerange_started_at_transfer_state", + ), + migrations.RemoveConstraint( + model_name="transferablerange", + name="eurydice_origin_core_transferablerange_finished_at_transfer_state", + ), + migrations.RemoveConstraint( + model_name="transferablerange", + name="eurydice_origin_core_transferablerange_started_at_created_at", + ), + migrations.RemoveConstraint( + model_name="transferablerange", + name="eurydice_origin_core_transferablerange_finished_at_started_at", + ), + migrations.RemoveField( + model_name="transferablerange", + name="started_at", + ), + migrations.AlterField( + model_name="transferablerange", + name="transfer_state", + field=models.CharField( + choices=[ + ("PENDING", "Pending"), + ("TRANSFERRED", "Transferred"), + ("ERROR", "Error"), + ], + default="PENDING", + help_text="The state of the transfer for this OutgoingTransferable", + max_length=11, + verbose_name="Transfer state", + ), + ), + migrations.AddConstraint( + model_name="transferablerange", + constraint=models.CheckConstraint( + check=models.Q( + models.Q( + ("finished_at__isnull", True), ("transfer_state", "PENDING") + ), + models.Q( + ("finished_at__isnull", False), + ("transfer_state__in", ("ERROR", "TRANSFERRED")), + ), + _connector="OR", + ), + name="eurydice_origin_core_transferablerange_finished_at_transfer_state", + ), + ), + migrations.AddConstraint( + model_name="transferablerange", + constraint=models.CheckConstraint( + check=models.Q( + ("finished_at__gte", django.db.models.expressions.F("created_at")) + ), + name="eurydice_origin_core_transferablerange_finished_at_created_at", + ), + ), + migrations.AlterField( + model_name="transferablerevocation", + name="transfer_state", + field=models.CharField( + choices=[ + ("PENDING", "Pending"), + ("TRANSFERRED", "Transferred"), + ("ERROR", "Error"), + ], + default="PENDING", + help_text="The reason for the OutgoingTransferable's revocation", + max_length=11, + verbose_name="State of the Revocation", + ), + ), + migrations.AlterModelOptions( + name="outgoingtransferable", + options={"base_manager_name": "objects"}, + ), + migrations.RenameField( + model_name="outgoingtransferable", + old_name="submission_success", + new_name="submission_succeeded_at", + ), + migrations.AlterField( + model_name="transferablerevocation", + name="transfer_state", + field=models.CharField( + choices=[ + ("PENDING", "Pending"), + ("TRANSFERRED", "Transferred"), + ("ERROR", "Error"), + ], + default="PENDING", + help_text="The OutgoingTransferable's revocation state", + max_length=11, + verbose_name="State of the Revocation", + ), + ), + migrations.RemoveConstraint( + model_name="transferablerange", + name="eurydice_origin_core_transferablerange_finished_at_transfer_state", + ), + migrations.AlterField( + model_name="transferablerange", + name="transfer_state", + field=models.CharField( + choices=[ + ("PENDING", "Pending"), + ("TRANSFERRED", "Transferred"), + ("CANCELED", "Canceled"), + ("ERROR", "Error"), + ], + default="PENDING", + help_text="The state of the transfer for this OutgoingTransferable", + max_length=11, + verbose_name="Transfer state", + ), + ), + migrations.AddConstraint( + model_name="transferablerange", + constraint=models.CheckConstraint( + check=models.Q( + models.Q( + ("finished_at__isnull", True), ("transfer_state", "PENDING") + ), + models.Q( + ("finished_at__isnull", False), + ("transfer_state__in", ("ERROR", "TRANSFERRED", "CANCELED")), + ), + _connector="OR", + ), + name="eurydice_origin_core_transferablerange_finished_at_transfer_state", + ), + ), + migrations.AddField( + model_name="outgoingtransferable", + name="bytes_received", + field=eurydice.common.models.fields.TransferableSizeField( + default=0, + help_text="The amount of bytes received until now for the Transferable", + validators=[django.core.validators.MaxValueValidator(5497558138880)], + verbose_name="Amount of bytes received", + ), + ), + migrations.AlterField( + model_name="outgoingtransferable", + name="size", + field=eurydice.common.models.fields.TransferableSizeField( + default=None, + help_text="The size in bytes of the file corresponding to the Transferable", + null=True, + validators=[django.core.validators.MaxValueValidator(5497558138880)], + verbose_name="Size in bytes", + ), + ), + migrations.AlterField( + model_name="outgoingtransferable", + name="created_at", + field=models.DateTimeField(auto_now_add=True, help_text="Creation date"), + ), + migrations.AlterField( + model_name="outgoingtransferable", + name="updated_at", + field=models.DateTimeField(auto_now=True, help_text="Update date"), + ), + migrations.AlterField( + model_name="transferablerange", + name="created_at", + field=models.DateTimeField(auto_now_add=True, help_text="Creation date"), + ), + migrations.AlterField( + model_name="transferablerange", + name="updated_at", + field=models.DateTimeField(auto_now=True, help_text="Update date"), + ), + migrations.AlterField( + model_name="transferablerevocation", + name="created_at", + field=models.DateTimeField(auto_now_add=True, help_text="Creation date"), + ), + migrations.AlterField( + model_name="transferablerevocation", + name="updated_at", + field=models.DateTimeField(auto_now=True, help_text="Update date"), + ), + migrations.AlterField( + model_name="userprofile", + name="created_at", + field=models.DateTimeField(auto_now_add=True, help_text="Creation date"), + ), + migrations.AlterField( + model_name="userprofile", + name="updated_at", + field=models.DateTimeField(auto_now=True, help_text="Update date"), + ), + migrations.AddConstraint( + model_name="outgoingtransferable", + constraint=models.CheckConstraint( + check=models.Q( + models.Q( + ("sha1__isnull", True), + ("submission_succeeded_at__isnull", True), + ), + models.Q( + ("sha1__isnull", False), + ("sha1__length", 20), + ("submission_succeeded_at__isnull", False), + ), + _connector="OR", + ), + name="eurydice_origin_core_outgoingtransferable_sha1", + ), + ), + migrations.AddConstraint( + model_name="outgoingtransferable", + constraint=models.CheckConstraint( + check=models.Q( + ("submission_succeeded_at__isnull", True), + ("bytes_received", django.db.models.expressions.F("size")), + _connector="OR", + ), + name="eurydice_origin_core_outgoingtransferable_bytes_received", + ), + ), + migrations.AddIndex( + model_name="transferablerange", + index=models.Index( + django.db.models.expressions.F("outgoing_transferable_id"), + condition=models.Q(("transfer_state", "PENDING")), + name="eurydice_pending_ranges", + ), + ), + migrations.AddIndex( + model_name="transferablerange", + index=models.Index( + django.db.models.expressions.F("outgoing_transferable_id"), + condition=models.Q(("transfer_state", "ERROR")), + name="eurydice_error_ranges", + ), + ), + migrations.AddIndex( + model_name="transferablerevocation", + index=models.Index( + django.db.models.expressions.F("outgoing_transferable_id"), + name="eurydice_revocations_fgn_key", + ), + ), + migrations.AddIndex( + model_name="outgoingtransferable", + index=models.Index( + fields=["-created_at"], name="eurydice_ou_created_55e483_idx" + ), + ), + migrations.AlterField( + model_name="transferablerange", + name="outgoing_transferable", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="transferable_ranges", + to="eurydice_origin_core.outgoingtransferable", + ), + ), + migrations.AlterField( + model_name="transferablerevocation", + name="outgoing_transferable", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="revocation", + to="eurydice_origin_core.outgoingtransferable", + ), + ), + migrations.AddField( + model_name="outgoingtransferable", + name="auto_bytes_transferred", + field=eurydice.common.models.fields.TransferableSizeField( + default=0, + help_text="The amount of this Transferable's bytes that have already been transferred (this field is kept up-to-date via database triggers)", + validators=[django.core.validators.MaxValueValidator(5497558138880)], + verbose_name="Amount of bytes transferred (auto-field)", + ), + ), + migrations.AddField( + model_name="outgoingtransferable", + name="auto_last_range_finished_at", + field=models.DateTimeField( + help_text="A timestamp indicating the end of the transfer of the last TransferableRange associated to this Transferable (this field is kept up-to-date via database triggers)", + null=True, + verbose_name="Last associated range finish date (auto-field)", + ), + ), + migrations.AddField( + model_name="outgoingtransferable", + name="auto_revocations_count", + field=models.PositiveIntegerField( + default=0, + help_text="The total amount of TransferableRevocations associated to this Transferable (this field is kept up-to-date via database triggers)", + verbose_name="Associated revocations count (auto-field)", + ), + ), + migrations.AddField( + model_name="outgoingtransferable", + name="auto_user_revocations_count", + field=models.PositiveIntegerField( + default=0, + help_text="The total amount of user cancelation TransferableRevocations associated to this Transferable (this field is kept up-to-date via database triggers)", + verbose_name="Associated CANCEL revocations count (auto-field)", + ), + ), + migrations.AddField( + model_name="outgoingtransferable", + name="auto_ranges_count", + field=models.PositiveIntegerField( + default=0, + help_text="The total amount of TransferableRanges associated to this Transferable (this field is kept up-to-date via database triggers)", + verbose_name="Associated ranges (auto-field)", + ), + ), + migrations.AddField( + model_name="outgoingtransferable", + name="auto_canceled_ranges_count", + field=models.PositiveIntegerField( + default=0, + help_text="The total amount of TransferableRanges in CANCELED state associated to this Transferable (this field is kept up-to-date via database triggers)", + verbose_name="Associated CANCELED ranges (auto-field)", + ), + ), + migrations.AddField( + model_name="outgoingtransferable", + name="auto_error_ranges_count", + field=models.PositiveIntegerField( + default=0, + help_text="The total amount of TransferableRanges in ERROR state associated to this Transferable (this field is kept up-to-date via database triggers)", + verbose_name="Associated ERROR ranges (auto-field)", + ), + ), + migrations.AddField( + model_name="outgoingtransferable", + name="auto_pending_ranges_count", + field=models.PositiveIntegerField( + default=0, + help_text="The total amount of TransferableRanges in PENDING state associated to this Transferable (this field is kept up-to-date via database triggers)", + verbose_name="Associated PENDING ranges (auto-field)", + ), + ), + migrations.AddField( + model_name="outgoingtransferable", + name="auto_transferred_ranges_count", + field=models.PositiveIntegerField( + default=0, + help_text="The total amount of TransferableRanges in TRANSFERRED state associated to this Transferable (this field is kept up-to-date via database triggers)", + verbose_name="Associated TRANSFERRED ranges (auto-field)", + ), + ), + migrations.RunSQL( + Path( + "eurydice/origin/core/migrations/triggers/" + "0018/initialize_auto_fields.sql" + ).read_text("utf-8"), + reverse_sql="", + ), + migrations.RunSQL( + Path( + "eurydice/origin/core/migrations/triggers/" + "0019/transferable_range_insert.sql" + ).read_text("utf-8"), + reverse_sql=Path( + "eurydice/origin/core/migrations/triggers/" + "0019/remove_transferable_range_insert.sql" + ).read_text("utf-8"), + ), + migrations.RunSQL( + Path( + "eurydice/origin/core/migrations/triggers/" + "0019/transferable_range_update.sql" + ).read_text("utf-8"), + reverse_sql=Path( + "eurydice/origin/core/migrations/triggers/" + "0019/remove_transferable_range_update.sql" + ).read_text("utf-8"), + ), + migrations.RunSQL( + Path( + "eurydice/origin/core/migrations/triggers/" + "0019/transferable_revocation_insert.sql" + ).read_text("utf-8"), + reverse_sql=Path( + "eurydice/origin/core/migrations/triggers/" + "0019/remove_transferable_revocation_insert.sql" + ).read_text("utf-8"), + ), + migrations.RunSQL( + Path( + "eurydice/origin/core/migrations/triggers/" + "0019/transferable_revocation_update.sql" + ).read_text("utf-8"), + reverse_sql=Path( + "eurydice/origin/core/migrations/triggers/" + "0019/remove_transferable_revocation_update.sql" + ).read_text("utf-8"), + ), + migrations.RunSQL( + Path( + "eurydice/origin/core/migrations/triggers/" + "0019/transferable_auto_fields_protection.sql" + ).read_text("utf-8"), + reverse_sql=Path( + "eurydice/origin/core/migrations/triggers/" + "0019/remove_transferable_auto_fields_protection.sql" + ).read_text("utf-8"), + ), + migrations.AddField( + model_name="outgoingtransferable", + name="auto_state_updated_at", + field=models.DateTimeField( + auto_now_add=True, + help_text="A timestamp indicating the last time the state of this Transferable changed", + verbose_name="Last state update date (auto-field)", + ), + ), + migrations.AddIndex( + model_name="outgoingtransferable", + index=models.Index( + fields=["-auto_state_updated_at"], name="eurydice_ou_auto_st_e29586_idx" + ), + ), + migrations.RunSQL( + Path( + "eurydice/origin/core/migrations/triggers/" + "0022/transferable_range_insert.sql", + ).read_text("utf-8"), + reverse_sql=Path( + "eurydice/origin/core/migrations/triggers/" + "0019/transferable_range_insert.sql", + ).read_text("utf-8"), + ), + migrations.RunSQL( + Path( + "eurydice/origin/core/migrations/triggers/" + "0022/transferable_range_update.sql", + ).read_text("utf-8"), + reverse_sql=Path( + "eurydice/origin/core/migrations/triggers/" + "0019/transferable_range_update.sql", + ).read_text("utf-8"), + ), + migrations.RunSQL( + Path( + "eurydice/origin/core/migrations/triggers/" + "0022/transferable_revocation_insert.sql", + ).read_text("utf-8"), + reverse_sql=Path( + "eurydice/origin/core/migrations/triggers/" + "0019/transferable_revocation_insert.sql", + ).read_text("utf-8"), + ), + migrations.RunSQL( + Path( + "eurydice/origin/core/migrations/triggers/" + "0022/transferable_update.sql", + ).read_text("utf-8"), + reverse_sql=Path( + "eurydice/origin/core/migrations/triggers/" + "0022/remove_transferable_update.sql", + ).read_text("utf-8"), + ), + migrations.RunSQL( + Path( + "eurydice/origin/core/migrations/triggers/" + "0022/transferable_auto_fields_protection.sql", + ).read_text("utf-8"), + reverse_sql=Path( + "eurydice/origin/core/migrations/triggers/" + "0019/transferable_auto_fields_protection.sql", + ).read_text("utf-8"), + ), + migrations.AlterField( + model_name="outgoingtransferable", + name="auto_bytes_transferred", + field=eurydice.common.models.fields.TransferableSizeField( + default=0, + editable=False, + help_text="The amount of this Transferable's bytes that have already been transferred (this field is kept up-to-date via database triggers)", + validators=[django.core.validators.MaxValueValidator(5497558138880)], + verbose_name="Amount of bytes transferred (auto-field)", + ), + ), + migrations.AlterField( + model_name="outgoingtransferable", + name="auto_canceled_ranges_count", + field=models.PositiveIntegerField( + default=0, + editable=False, + help_text="The total amount of TransferableRanges in CANCELED state associated to this Transferable (this field is kept up-to-date via database triggers)", + verbose_name="Associated CANCELED ranges (auto-field)", + ), + ), + migrations.AlterField( + model_name="outgoingtransferable", + name="auto_error_ranges_count", + field=models.PositiveIntegerField( + default=0, + editable=False, + help_text="The total amount of TransferableRanges in ERROR state associated to this Transferable (this field is kept up-to-date via database triggers)", + verbose_name="Associated ERROR ranges (auto-field)", + ), + ), + migrations.AlterField( + model_name="outgoingtransferable", + name="auto_last_range_finished_at", + field=models.DateTimeField( + editable=False, + help_text="A timestamp indicating the end of the transfer of the last TransferableRange associated to this Transferable (this field is kept up-to-date via database triggers)", + null=True, + verbose_name="Last associated range finish date (auto-field)", + ), + ), + migrations.AlterField( + model_name="outgoingtransferable", + name="auto_pending_ranges_count", + field=models.PositiveIntegerField( + default=0, + editable=False, + help_text="The total amount of TransferableRanges in PENDING state associated to this Transferable (this field is kept up-to-date via database triggers)", + verbose_name="Associated PENDING ranges (auto-field)", + ), + ), + migrations.AlterField( + model_name="outgoingtransferable", + name="auto_ranges_count", + field=models.PositiveIntegerField( + default=0, + editable=False, + help_text="The total amount of TransferableRanges associated to this Transferable (this field is kept up-to-date via database triggers)", + verbose_name="Associated ranges (auto-field)", + ), + ), + migrations.AlterField( + model_name="outgoingtransferable", + name="auto_revocations_count", + field=models.PositiveIntegerField( + default=0, + editable=False, + help_text="The total amount of TransferableRevocations associated to this Transferable (this field is kept up-to-date via database triggers)", + verbose_name="Associated revocations count (auto-field)", + ), + ), + migrations.AlterField( + model_name="outgoingtransferable", + name="auto_transferred_ranges_count", + field=models.PositiveIntegerField( + default=0, + editable=False, + help_text="The total amount of TransferableRanges in TRANSFERRED state associated to this Transferable (this field is kept up-to-date via database triggers)", + verbose_name="Associated TRANSFERRED ranges (auto-field)", + ), + ), + migrations.AlterField( + model_name="outgoingtransferable", + name="auto_user_revocations_count", + field=models.PositiveIntegerField( + default=0, + editable=False, + help_text="The total amount of user cancelation TransferableRevocations associated to this Transferable (this field is kept up-to-date via database triggers)", + verbose_name="Associated CANCEL revocations count (auto-field)", + ), + ), + ] diff --git a/backend/eurydice/origin/core/migrations/0024_maintenance.py b/backend/eurydice/origin/core/migrations/0024_maintenance.py new file mode 100644 index 0000000..3962779 --- /dev/null +++ b/backend/eurydice/origin/core/migrations/0024_maintenance.py @@ -0,0 +1,48 @@ +# Generated by Django 4.2.2 on 2023-06-23 15:11 + +import uuid + +from django.db import migrations +from django.db import models + + +def init_maintenance(apps, schema_editor): + Maintenance = apps.get_model("eurydice_origin_core", "Maintenance") + Maintenance.objects.create(maintenance=False) + + +class Migration(migrations.Migration): + dependencies = [ + ("eurydice_origin_core", "0001_squashed_0023_make_auto_fields_non_editable"), + ] + + operations = [ + migrations.CreateModel( + name="Maintenance", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "_singleton", + models.BooleanField(default=True, editable=False, unique=True), + ), + ( + "maintenance", + models.BooleanField( + verbose_name="Whether the app is currently in maintenance mode." + ), + ), + ], + options={ + "db_table": "eurydice_maintenance", + }, + ), + migrations.RunPython(init_maintenance), + ] diff --git a/backend/eurydice/origin/core/migrations/0025_remove_outgoingtransferable_updated_at_and_more.py b/backend/eurydice/origin/core/migrations/0025_remove_outgoingtransferable_updated_at_and_more.py new file mode 100644 index 0000000..8b148d9 --- /dev/null +++ b/backend/eurydice/origin/core/migrations/0025_remove_outgoingtransferable_updated_at_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.2 on 2023-07-05 15:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("eurydice_origin_core", "0024_maintenance"), + ] + + operations = [ + migrations.RemoveField( + model_name="outgoingtransferable", + name="updated_at", + ), + migrations.RemoveField( + model_name="transferablerange", + name="updated_at", + ), + migrations.RemoveField( + model_name="transferablerevocation", + name="updated_at", + ), + migrations.RemoveField( + model_name="userprofile", + name="updated_at", + ), + ] diff --git a/backend/eurydice/origin/core/migrations/0026_alter_transferablerange_byte_offset.py b/backend/eurydice/origin/core/migrations/0026_alter_transferablerange_byte_offset.py new file mode 100644 index 0000000..52db349 --- /dev/null +++ b/backend/eurydice/origin/core/migrations/0026_alter_transferablerange_byte_offset.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.4 on 2023-08-30 13:23 + +import django.core.validators +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "eurydice_origin_core", + "0025_remove_outgoingtransferable_updated_at_and_more", + ), + ] + + operations = [ + migrations.AlterField( + model_name="transferablerange", + name="byte_offset", + field=models.PositiveBigIntegerField( + help_text="The start position of this range in the associated Transferable", + validators=[django.core.validators.MaxValueValidator(54975581388800)], + verbose_name="Byte offset", + ), + ), + ] diff --git a/backend/eurydice/origin/core/migrations/0027_user_last_access.py b/backend/eurydice/origin/core/migrations/0027_user_last_access.py new file mode 100644 index 0000000..b988d81 --- /dev/null +++ b/backend/eurydice/origin/core/migrations/0027_user_last_access.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.5 on 2023-10-12 09:07 + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("eurydice_origin_core", "0026_alter_transferablerange_byte_offset"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="last_access", + field=models.DateTimeField( + blank=True, + help_text="Unlike last_login, this field gets updated every time the user accesses the API, even when they authenticate with an API token.", + null=True, + verbose_name="last access", + ), + ), + ] diff --git a/backend/eurydice/origin/core/migrations/0028_lastpacketsentat.py b/backend/eurydice/origin/core/migrations/0028_lastpacketsentat.py new file mode 100644 index 0000000..2f3a213 --- /dev/null +++ b/backend/eurydice/origin/core/migrations/0028_lastpacketsentat.py @@ -0,0 +1,40 @@ +# Generated by Django 4.2.2 on 2023-07-28 16:10 + +import uuid + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ( + "eurydice_origin_core", + "0027_user_last_access", + ), + ] + + operations = [ + migrations.CreateModel( + name="LastPacketSentAt", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ( + "_singleton", + models.BooleanField(default=True, editable=False, unique=True), + ), + ("timestamp", models.DateTimeField(auto_now=True)), + ], + options={ + "db_table": "eurydice_last_packet_sent_at", + }, + ), + ] diff --git a/backend/eurydice/origin/core/migrations/__init__.py b/backend/eurydice/origin/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/eurydice/origin/core/migrations/triggers/0018/initialize_auto_fields.sql b/backend/eurydice/origin/core/migrations/triggers/0018/initialize_auto_fields.sql new file mode 100644 index 0000000..eaf32a7 --- /dev/null +++ b/backend/eurydice/origin/core/migrations/triggers/0018/initialize_auto_fields.sql @@ -0,0 +1,155 @@ +-- Initialize auto_bytes_transferred +UPDATE + eurydice_outgoing_transferables t1 +SET + auto_bytes_transferred = t2.bytes_transferred +FROM ( + SELECT + SUM(size) AS bytes_transferred, + outgoing_transferable_id + FROM + eurydice_transferable_ranges + WHERE + transfer_state = 'TRANSFERRED' + GROUP BY + outgoing_transferable_id) t2 +WHERE + t1.id = t2.outgoing_transferable_id; + +-- Initialize auto_last_range_finished_at +UPDATE + eurydice_outgoing_transferables t1 +SET + auto_last_range_finished_at = t2.last_range_finished_at +FROM ( + SELECT + MAX(finished_at) AS last_range_finished_at, + outgoing_transferable_id + FROM + eurydice_transferable_ranges + GROUP BY + outgoing_transferable_id) t2 +WHERE + t1.id = t2.outgoing_transferable_id; + +-- Initialize auto_revocations_count +UPDATE + eurydice_outgoing_transferables t1 +SET + auto_revocations_count = t2.revocations_count +FROM ( + SELECT + COUNT(id) AS revocations_count, + outgoing_transferable_id + FROM + eurydice_transferable_revocations + GROUP BY + outgoing_transferable_id) t2 +WHERE + t1.id = t2.outgoing_transferable_id; + +-- Initialize auto_user_revocations_count +UPDATE + eurydice_outgoing_transferables t1 +SET + auto_user_revocations_count = t2.user_revocations_count +FROM ( + SELECT + COUNT(id) AS user_revocations_count, + outgoing_transferable_id + FROM + eurydice_transferable_revocations + WHERE + transfer_state = 'USER_CANCELED' + GROUP BY + outgoing_transferable_id) t2 +WHERE + t1.id = t2.outgoing_transferable_id; + +-- Initialize auto_ranges_count +UPDATE + eurydice_outgoing_transferables t1 +SET + auto_ranges_count = t2.ranges_count +FROM ( + SELECT + COUNT(id) AS ranges_count, + outgoing_transferable_id + FROM + eurydice_transferable_ranges + GROUP BY + outgoing_transferable_id) t2 +WHERE + t1.id = t2.outgoing_transferable_id; + +-- Initialize auto_canceled_ranges_count +UPDATE + eurydice_outgoing_transferables t1 +SET + auto_canceled_ranges_count = t2.canceled_ranges_count +FROM ( + SELECT + COUNT(id) AS canceled_ranges_count, + outgoing_transferable_id + FROM + eurydice_transferable_ranges + WHERE + transfer_state = 'CANCELED' + GROUP BY + outgoing_transferable_id) t2 +WHERE + t1.id = t2.outgoing_transferable_id; + +-- Initialize auto_error_ranges_count +UPDATE + eurydice_outgoing_transferables t1 +SET + auto_error_ranges_count = t2.error_ranges_count +FROM ( + SELECT + COUNT(id) AS error_ranges_count, + outgoing_transferable_id + FROM + eurydice_transferable_ranges + WHERE + transfer_state = 'ERROR' + GROUP BY + outgoing_transferable_id) t2 +WHERE + t1.id = t2.outgoing_transferable_id; + +-- Initialize auto_pending_ranges_count +UPDATE + eurydice_outgoing_transferables t1 +SET + auto_pending_ranges_count = t2.pending_ranges_count +FROM ( + SELECT + COUNT(id) AS pending_ranges_count, + outgoing_transferable_id + FROM + eurydice_transferable_ranges + WHERE + transfer_state = 'PENDING' + GROUP BY + outgoing_transferable_id) t2 +WHERE + t1.id = t2.outgoing_transferable_id; + +-- Initialize auto_transferred_ranges_count +UPDATE + eurydice_outgoing_transferables t1 +SET + auto_transferred_ranges_count = t2.transferred_ranges_count +FROM ( + SELECT + COUNT(id) AS transferred_ranges_count, + outgoing_transferable_id + FROM + eurydice_transferable_ranges + WHERE + transfer_state = 'TRANSFERRED' + GROUP BY + outgoing_transferable_id) t2 +WHERE + t1.id = t2.outgoing_transferable_id diff --git a/backend/eurydice/origin/core/migrations/triggers/0019/remove_transferable_auto_fields_protection.sql b/backend/eurydice/origin/core/migrations/triggers/0019/remove_transferable_auto_fields_protection.sql new file mode 100644 index 0000000..3137d9d --- /dev/null +++ b/backend/eurydice/origin/core/migrations/triggers/0019/remove_transferable_auto_fields_protection.sql @@ -0,0 +1,4 @@ +DROP TRIGGER on_auto_field_update ON eurydice_outgoing_transferables; + +DROP FUNCTION trigger_on_auto_field_update (); + diff --git a/backend/eurydice/origin/core/migrations/triggers/0019/remove_transferable_range_insert.sql b/backend/eurydice/origin/core/migrations/triggers/0019/remove_transferable_range_insert.sql new file mode 100644 index 0000000..1df6b7a --- /dev/null +++ b/backend/eurydice/origin/core/migrations/triggers/0019/remove_transferable_range_insert.sql @@ -0,0 +1,4 @@ +DROP TRIGGER on_range_insert ON eurydice_transferable_ranges; + +DROP FUNCTION trigger_on_range_insert (); + diff --git a/backend/eurydice/origin/core/migrations/triggers/0019/remove_transferable_range_update.sql b/backend/eurydice/origin/core/migrations/triggers/0019/remove_transferable_range_update.sql new file mode 100644 index 0000000..e5dfa60 --- /dev/null +++ b/backend/eurydice/origin/core/migrations/triggers/0019/remove_transferable_range_update.sql @@ -0,0 +1,4 @@ +DROP TRIGGER on_range_update ON eurydice_transferable_ranges; + +DROP FUNCTION trigger_on_range_update (); + diff --git a/backend/eurydice/origin/core/migrations/triggers/0019/remove_transferable_revocation_insert.sql b/backend/eurydice/origin/core/migrations/triggers/0019/remove_transferable_revocation_insert.sql new file mode 100644 index 0000000..2e806b2 --- /dev/null +++ b/backend/eurydice/origin/core/migrations/triggers/0019/remove_transferable_revocation_insert.sql @@ -0,0 +1,4 @@ +DROP TRIGGER on_revocation_insert ON eurydice_transferable_revocations; + +DROP FUNCTION trigger_on_revocation_insert (); + diff --git a/backend/eurydice/origin/core/migrations/triggers/0019/remove_transferable_revocation_update.sql b/backend/eurydice/origin/core/migrations/triggers/0019/remove_transferable_revocation_update.sql new file mode 100644 index 0000000..f51d849 --- /dev/null +++ b/backend/eurydice/origin/core/migrations/triggers/0019/remove_transferable_revocation_update.sql @@ -0,0 +1,4 @@ +DROP TRIGGER on_revocation_update ON eurydice_transferable_revocations; + +DROP FUNCTION trigger_on_revocation_update (); + diff --git a/backend/eurydice/origin/core/migrations/triggers/0019/transferable_auto_fields_protection.sql b/backend/eurydice/origin/core/migrations/triggers/0019/transferable_auto_fields_protection.sql new file mode 100644 index 0000000..b37c3cf --- /dev/null +++ b/backend/eurydice/origin/core/migrations/triggers/0019/transferable_auto_fields_protection.sql @@ -0,0 +1,30 @@ +CREATE OR REPLACE FUNCTION trigger_on_auto_field_update () + RETURNS TRIGGER + AS $$ +BEGIN + -- + -- Raise an exception whenever an 'auto_' field is directly UPDATED through a query; + -- only database triggers are allowed to modify 'auto_' fields, hence the + -- `WHEN (pg_trigger_depth() < 1)` check + -- + RAISE EXCEPTION 'SQL queries are not allowed to update auto-field themselves'; +END; +$$ +LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS on_auto_field_update ON eurydice_outgoing_transferables; + +CREATE TRIGGER on_auto_field_update + AFTER UPDATE OF auto_revocations_count, + auto_user_revocations_count, + auto_ranges_count, + auto_pending_ranges_count, + auto_transferred_ranges_count, + auto_canceled_ranges_count, + auto_error_ranges_count, + auto_last_range_finished_at, + auto_bytes_transferred ON eurydice_outgoing_transferables + FOR EACH ROW + WHEN (pg_trigger_depth() < 1) + EXECUTE PROCEDURE trigger_on_auto_field_update (); + diff --git a/backend/eurydice/origin/core/migrations/triggers/0019/transferable_range_insert.sql b/backend/eurydice/origin/core/migrations/triggers/0019/transferable_range_insert.sql new file mode 100644 index 0000000..2cb3cd7 --- /dev/null +++ b/backend/eurydice/origin/core/migrations/triggers/0019/transferable_range_insert.sql @@ -0,0 +1,30 @@ +CREATE OR REPLACE FUNCTION trigger_on_range_insert () + RETURNS TRIGGER + AS $$ +BEGIN + -- + -- Update 'auto_' fields in the OutgoingTransferable associated to newly + -- inserted TransferableRanges + -- + IF (NEW.transfer_state != 'PENDING') THEN + RAISE EXCEPTION 'TransferableRanges are supposed to be created in PENDING state'; + END IF; + UPDATE + eurydice_outgoing_transferables + SET + auto_ranges_count = auto_ranges_count + 1, + auto_pending_ranges_count = auto_pending_ranges_count + 1 + WHERE + id = NEW.outgoing_transferable_id; + RETURN NULL; +END; +$$ +LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS on_range_insert ON eurydice_transferable_ranges; + +CREATE TRIGGER on_range_insert + AFTER INSERT ON eurydice_transferable_ranges + FOR EACH ROW + EXECUTE PROCEDURE trigger_on_range_insert (); + diff --git a/backend/eurydice/origin/core/migrations/triggers/0019/transferable_range_update.sql b/backend/eurydice/origin/core/migrations/triggers/0019/transferable_range_update.sql new file mode 100644 index 0000000..bfa2c06 --- /dev/null +++ b/backend/eurydice/origin/core/migrations/triggers/0019/transferable_range_update.sql @@ -0,0 +1,66 @@ +CREATE OR REPLACE FUNCTION trigger_on_range_update () + RETURNS TRIGGER + AS $$ +BEGIN + -- + -- Update 'auto_' fields in the OutgoingTransferable associated to + -- updated TransferableRanges + -- + -- Below are strict rules about when a TransferableRange is allowed to change ; + -- these rules were made so that the code below does not have to handle subtle cases + -- such as updating the size of an already TRANSFERRED range, or the finished_at + -- field of a range already in a final state + IF (OLD.outgoing_transferable_id != NEW.outgoing_transferable_id) THEN + RAISE EXCEPTION 'TransferableRanges should not change their associated OutgoingTransferable'; + END IF; + IF (OLD.transfer_state != 'PENDING') THEN + RAISE EXCEPTION 'TransferableRanges should not change if they are not PENDING'; + END IF; + IF NEW.transfer_state = 'PENDING' THEN + RAISE EXCEPTION 'TransferableRanges should not change if their state does not change'; + END IF; + IF NEW.transfer_state = 'TRANSFERRED' THEN + UPDATE + eurydice_outgoing_transferables + SET + auto_bytes_transferred = auto_bytes_transferred + NEW.size, + auto_pending_ranges_count = auto_pending_ranges_count - 1, + auto_transferred_ranges_count = auto_transferred_ranges_count + 1, + auto_last_range_finished_at = NEW.finished_at + WHERE + id = NEW.outgoing_transferable_id; + RETURN NULL; + END IF; + IF NEW.transfer_state = 'ERROR' THEN + UPDATE + eurydice_outgoing_transferables + SET + auto_pending_ranges_count = auto_pending_ranges_count - 1, + auto_error_ranges_count = auto_error_ranges_count + 1, + auto_last_range_finished_at = NEW.finished_at + WHERE + id = NEW.outgoing_transferable_id; + RETURN NULL; + END IF; + IF NEW.transfer_state = 'CANCELED' THEN + UPDATE + eurydice_outgoing_transferables + SET + auto_pending_ranges_count = auto_pending_ranges_count - 1, + auto_canceled_ranges_count = auto_canceled_ranges_count + 1, + auto_last_range_finished_at = NEW.finished_at + WHERE + id = NEW.outgoing_transferable_id; + RETURN NULL; + END IF; +END; +$$ +LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS on_range_update ON eurydice_transferable_ranges; + +CREATE TRIGGER on_range_update + AFTER UPDATE ON eurydice_transferable_ranges + FOR EACH ROW + EXECUTE PROCEDURE trigger_on_range_update (); + diff --git a/backend/eurydice/origin/core/migrations/triggers/0019/transferable_revocation_insert.sql b/backend/eurydice/origin/core/migrations/triggers/0019/transferable_revocation_insert.sql new file mode 100644 index 0000000..d5c82d3 --- /dev/null +++ b/backend/eurydice/origin/core/migrations/triggers/0019/transferable_revocation_insert.sql @@ -0,0 +1,36 @@ +CREATE OR REPLACE FUNCTION trigger_on_revocation_insert () + RETURNS TRIGGER + AS $$ +BEGIN + -- + -- Update 'auto_' fields in the OutgoingTransferable associated to + -- newly insterted TransferableRevocations + -- + IF NEW.reason = 'USER_CANCELED' THEN + UPDATE + eurydice_outgoing_transferables + SET + auto_revocations_count = auto_revocations_count + 1, + auto_user_revocations_count = auto_user_revocations_count + 1 + WHERE + id = NEW.outgoing_transferable_id; + RETURN NULL; + ELSE + UPDATE + eurydice_outgoing_transferables + SET + auto_revocations_count = auto_revocations_count + 1 + WHERE + id = NEW.outgoing_transferable_id; + RETURN NULL; + END IF; +END; +$$ +LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS on_revocation_insert ON eurydice_transferable_revocations; + +CREATE TRIGGER on_revocation_insert + AFTER INSERT ON eurydice_transferable_revocations + FOR EACH ROW + EXECUTE PROCEDURE trigger_on_revocation_insert (); diff --git a/backend/eurydice/origin/core/migrations/triggers/0019/transferable_revocation_update.sql b/backend/eurydice/origin/core/migrations/triggers/0019/transferable_revocation_update.sql new file mode 100644 index 0000000..93b9b18 --- /dev/null +++ b/backend/eurydice/origin/core/migrations/triggers/0019/transferable_revocation_update.sql @@ -0,0 +1,21 @@ +CREATE OR REPLACE FUNCTION trigger_on_revocation_update () + RETURNS TRIGGER + AS $$ +BEGIN + -- + -- Raise an exception if a TransferableRevocation is changed after its creation + -- + RAISE EXCEPTION 'TransferableRevocations should not include their associated OutgoingTransferable nor their reason in UPDATE clauses'; +END; +$$ +LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS on_revocation_update ON eurydice_transferable_revocations; + +CREATE TRIGGER on_revocation_update + AFTER UPDATE OF outgoing_transferable_id, + reason + ON eurydice_transferable_revocations + FOR EACH ROW + EXECUTE PROCEDURE trigger_on_revocation_update (); + diff --git a/backend/eurydice/origin/core/migrations/triggers/0022/remove_transferable_update.sql b/backend/eurydice/origin/core/migrations/triggers/0022/remove_transferable_update.sql new file mode 100644 index 0000000..942203c --- /dev/null +++ b/backend/eurydice/origin/core/migrations/triggers/0022/remove_transferable_update.sql @@ -0,0 +1,4 @@ +DROP TRIGGER on_transferable_update ON eurydice_outgoing_transferables; + +DROP FUNCTION trigger_on_transferable_update (); + diff --git a/backend/eurydice/origin/core/migrations/triggers/0022/transferable_auto_fields_protection.sql b/backend/eurydice/origin/core/migrations/triggers/0022/transferable_auto_fields_protection.sql new file mode 100644 index 0000000..c660c6f --- /dev/null +++ b/backend/eurydice/origin/core/migrations/triggers/0022/transferable_auto_fields_protection.sql @@ -0,0 +1,31 @@ +CREATE OR REPLACE FUNCTION trigger_on_auto_field_update () + RETURNS TRIGGER + AS $$ +BEGIN + -- + -- Raise an exception whenever an 'auto_' field is directly UPDATED through a query; + -- only database triggers are allowed to modify 'auto_' fields, hence the + -- `WHEN (pg_trigger_depth() < 1)` check + -- + RAISE EXCEPTION 'SQL queries are not allowed to update auto-field themselves'; +END; +$$ +LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS on_auto_field_update ON eurydice_outgoing_transferables; + +CREATE TRIGGER on_auto_field_update + AFTER UPDATE OF auto_revocations_count, + auto_user_revocations_count, + auto_ranges_count, + auto_pending_ranges_count, + auto_transferred_ranges_count, + auto_canceled_ranges_count, + auto_error_ranges_count, + auto_last_range_finished_at, + auto_bytes_transferred, + auto_state_updated_at ON eurydice_outgoing_transferables + FOR EACH ROW + WHEN (pg_trigger_depth() < 1) + EXECUTE PROCEDURE trigger_on_auto_field_update (); + diff --git a/backend/eurydice/origin/core/migrations/triggers/0022/transferable_range_insert.sql b/backend/eurydice/origin/core/migrations/triggers/0022/transferable_range_insert.sql new file mode 100644 index 0000000..2fe4f59 --- /dev/null +++ b/backend/eurydice/origin/core/migrations/triggers/0022/transferable_range_insert.sql @@ -0,0 +1,31 @@ +CREATE OR REPLACE FUNCTION trigger_on_range_insert () + RETURNS TRIGGER + AS $$ +BEGIN + -- + -- Update 'auto_' fields in the OutgoingTransferable associated to newly + -- inserted TransferableRanges + -- + IF (NEW.transfer_state != 'PENDING') THEN + RAISE EXCEPTION 'TransferableRanges are supposed to be created in PENDING state'; + END IF; + UPDATE + eurydice_outgoing_transferables + SET + auto_ranges_count = auto_ranges_count + 1, + auto_pending_ranges_count = auto_pending_ranges_count + 1, + auto_state_updated_at = NEW.created_at + WHERE + id = NEW.outgoing_transferable_id; + RETURN NULL; +END; +$$ +LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS on_range_insert ON eurydice_transferable_ranges; + +CREATE TRIGGER on_range_insert + AFTER INSERT ON eurydice_transferable_ranges + FOR EACH ROW + EXECUTE PROCEDURE trigger_on_range_insert (); + diff --git a/backend/eurydice/origin/core/migrations/triggers/0022/transferable_range_update.sql b/backend/eurydice/origin/core/migrations/triggers/0022/transferable_range_update.sql new file mode 100644 index 0000000..45aecde --- /dev/null +++ b/backend/eurydice/origin/core/migrations/triggers/0022/transferable_range_update.sql @@ -0,0 +1,69 @@ +CREATE OR REPLACE FUNCTION trigger_on_range_update () + RETURNS TRIGGER + AS $$ +BEGIN + -- + -- Update 'auto_' fields in the OutgoingTransferable associated to + -- updated TransferableRanges + -- + -- Below are strict rules about when a TransferableRange is allowed to change ; + -- these rules were made so that the code below does not have to handle subtle cases + -- such as updating the size of an already TRANSFERRED range, or the finished_at + -- field of a range already in a final state + IF (OLD.outgoing_transferable_id != NEW.outgoing_transferable_id) THEN + RAISE EXCEPTION 'TransferableRanges should not change their associated OutgoingTransferable'; + END IF; + IF (OLD.transfer_state != 'PENDING') THEN + RAISE EXCEPTION 'TransferableRanges should not change if they are not PENDING'; + END IF; + IF NEW.transfer_state = 'PENDING' THEN + RAISE EXCEPTION 'TransferableRanges should not change if their state does not change'; + END IF; + IF NEW.transfer_state = 'TRANSFERRED' THEN + UPDATE + eurydice_outgoing_transferables + SET + auto_bytes_transferred = auto_bytes_transferred + NEW.size, + auto_pending_ranges_count = auto_pending_ranges_count - 1, + auto_transferred_ranges_count = auto_transferred_ranges_count + 1, + auto_last_range_finished_at = NEW.finished_at, + auto_state_updated_at = NEW.finished_at + WHERE + id = NEW.outgoing_transferable_id; + RETURN NULL; + END IF; + IF NEW.transfer_state = 'ERROR' THEN + UPDATE + eurydice_outgoing_transferables + SET + auto_pending_ranges_count = auto_pending_ranges_count - 1, + auto_error_ranges_count = auto_error_ranges_count + 1, + auto_last_range_finished_at = NEW.finished_at, + auto_state_updated_at = NEW.finished_at + WHERE + id = NEW.outgoing_transferable_id; + RETURN NULL; + END IF; + IF NEW.transfer_state = 'CANCELED' THEN + UPDATE + eurydice_outgoing_transferables + SET + auto_pending_ranges_count = auto_pending_ranges_count - 1, + auto_canceled_ranges_count = auto_canceled_ranges_count + 1, + auto_last_range_finished_at = NEW.finished_at, + auto_state_updated_at = NEW.finished_at + WHERE + id = NEW.outgoing_transferable_id; + RETURN NULL; + END IF; +END; +$$ +LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS on_range_update ON eurydice_transferable_ranges; + +CREATE TRIGGER on_range_update + AFTER UPDATE ON eurydice_transferable_ranges + FOR EACH ROW + EXECUTE PROCEDURE trigger_on_range_update (); + diff --git a/backend/eurydice/origin/core/migrations/triggers/0022/transferable_revocation_insert.sql b/backend/eurydice/origin/core/migrations/triggers/0022/transferable_revocation_insert.sql new file mode 100644 index 0000000..95cbb9c --- /dev/null +++ b/backend/eurydice/origin/core/migrations/triggers/0022/transferable_revocation_insert.sql @@ -0,0 +1,38 @@ +CREATE OR REPLACE FUNCTION trigger_on_revocation_insert () + RETURNS TRIGGER + AS $$ +BEGIN + -- + -- Update 'auto_' fields in the OutgoingTransferable associated to + -- newly insterted TransferableRevocations + -- + IF NEW.reason = 'USER_CANCELED' THEN + UPDATE + eurydice_outgoing_transferables + SET + auto_revocations_count = auto_revocations_count + 1, + auto_user_revocations_count = auto_user_revocations_count + 1, + auto_state_updated_at = NEW.created_at + WHERE + id = NEW.outgoing_transferable_id; + RETURN NULL; + ELSE + UPDATE + eurydice_outgoing_transferables + SET + auto_revocations_count = auto_revocations_count + 1, + auto_state_updated_at = NEW.created_at + WHERE + id = NEW.outgoing_transferable_id; + RETURN NULL; + END IF; +END; +$$ +LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS on_revocation_insert ON eurydice_transferable_revocations; + +CREATE TRIGGER on_revocation_insert + AFTER INSERT ON eurydice_transferable_revocations + FOR EACH ROW + EXECUTE PROCEDURE trigger_on_revocation_insert (); diff --git a/backend/eurydice/origin/core/migrations/triggers/0022/transferable_update.sql b/backend/eurydice/origin/core/migrations/triggers/0022/transferable_update.sql new file mode 100644 index 0000000..00489b4 --- /dev/null +++ b/backend/eurydice/origin/core/migrations/triggers/0022/transferable_update.sql @@ -0,0 +1,23 @@ +CREATE OR REPLACE FUNCTION trigger_on_transferable_update () + RETURNS TRIGGER + AS $$ +BEGIN + -- + -- Update 'auto_' fields in updated OutgoingTransferables + -- + IF OLD.submission_succeeded_at IS NULL AND + NEW.submission_succeeded_at IS NOT NULL THEN + NEW.auto_state_updated_at := NEW.submission_succeeded_at; + END IF; + RETURN NEW; +END; +$$ +LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS on_transferable_update ON eurydice_outgoing_transferables; + +CREATE TRIGGER on_transferable_update + BEFORE UPDATE ON eurydice_outgoing_transferables + FOR EACH ROW + EXECUTE PROCEDURE trigger_on_transferable_update (); + diff --git a/backend/eurydice/origin/core/models/__init__.py b/backend/eurydice/origin/core/models/__init__.py new file mode 100644 index 0000000..be8458b --- /dev/null +++ b/backend/eurydice/origin/core/models/__init__.py @@ -0,0 +1,21 @@ +""" +Database models specific to the API on the origin side +""" + +from .last_packet_sent_at import LastPacketSentAt +from .maintenance import Maintenance +from .outgoing_transferable import OutgoingTransferable +from .transferable_range import TransferableRange +from .transferable_revocation import TransferableRevocation +from .user import User +from .user import UserProfile + +__all__ = ( + "User", + "UserProfile", + "Maintenance", + "LastPacketSentAt", + "OutgoingTransferable", + "TransferableRange", + "TransferableRevocation", +) diff --git a/backend/eurydice/origin/core/models/last_packet_sent_at.py b/backend/eurydice/origin/core/models/last_packet_sent_at.py new file mode 100644 index 0000000..8cfb0ce --- /dev/null +++ b/backend/eurydice/origin/core/models/last_packet_sent_at.py @@ -0,0 +1,8 @@ +from eurydice.common import models as common_models + + +class LastPacketSentAt(common_models.TimestampSingleton): + """Singleton holding the date of the last sent packet (data or heartbeat).""" + + class Meta: + db_table = "eurydice_last_packet_sent_at" diff --git a/backend/eurydice/origin/core/models/maintenance.py b/backend/eurydice/origin/core/models/maintenance.py new file mode 100644 index 0000000..c46df3f --- /dev/null +++ b/backend/eurydice/origin/core/models/maintenance.py @@ -0,0 +1,23 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from eurydice.common import models as common_models + + +class Maintenance(common_models.SingletonModel): + """Singleton holding whether the app is in maintenance mode.""" + + maintenance = models.BooleanField( + _("Whether the app is currently in maintenance mode.") + ) + + class Meta: + db_table = "eurydice_maintenance" + + @classmethod + def is_maintenance(cls) -> bool: + """Returns whether the app is currently in maintenance mode.""" + try: + return cls.objects.get().maintenance + except cls.DoesNotExist: + return False diff --git a/backend/eurydice/origin/core/models/outgoing_transferable.py b/backend/eurydice/origin/core/models/outgoing_transferable.py new file mode 100644 index 0000000..4271f8c --- /dev/null +++ b/backend/eurydice/origin/core/models/outgoing_transferable.py @@ -0,0 +1,452 @@ +import datetime +from typing import Any +from typing import Iterable +from typing import Optional + +from django.db import models +from django.db.models import Q +from django.db.models import expressions +from django.db.models import functions +from django.db.models import query +from django.db.models.expressions import F +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ + +from eurydice.common import enums +from eurydice.common import models as common_models + + +class Epoch(expressions.Func): + """Django ORM database function for converting a duration to an integer + count of seconds. + """ + + template = "EXTRACT(epoch FROM %(expressions)s)::INTEGER" + output_field: models.IntegerField = models.IntegerField() + + +class ConvertSecondsToDuration(expressions.Func): + """Django ORM database function for converting an integer count of + seconds to a duration. + """ + + template = "make_interval(secs => %(expressions)s)" + output_field: models.DurationField = models.DurationField() + + +class PythonNow(models.Value): + """Django ORM value used for annotating the current python timestamp as opposed + to the current database timestamp as provided by `django.db.functions.Now` + """ + + def __init__(self, value: Optional[datetime.datetime] = None, **kwargs: Any): + super().__init__(value, **kwargs) + + def as_sql(self, *args, **kwargs): # noqa: ANN201 + self.value = timezone.now() + return super().as_sql(*args, **kwargs) + + +def _build_state_annotation() -> expressions.Case: + """Build the annotation for computing the state when querying the + OutgoingTransferable(s). + + The `state` is pure business logic computed in the DB. + + Returns: + expressions.Case: the Django ORM conditions for computing + the OutgoingTransferable's state + + """ + + return expressions.Case( + expressions.When( + auto_user_revocations_count__gt=0, + then=expressions.Value(enums.OutgoingTransferableState.CANCELED), + ), + expressions.When( + Q(auto_error_ranges_count__gt=0) | Q(auto_revocations_count__gt=0), + then=expressions.Value(enums.OutgoingTransferableState.ERROR), + ), + expressions.When( + Q( + submission_succeeded_at__isnull=False, + auto_pending_ranges_count=0, + auto_canceled_ranges_count=0, + auto_error_ranges_count=0, + ), + then=expressions.Value(enums.OutgoingTransferableState.SUCCESS), + ), + expressions.When( + Q(auto_ranges_count=0) + | Q( + auto_pending_ranges_count__gt=0, + auto_transferred_ranges_count=0, + auto_canceled_ranges_count=0, + auto_error_ranges_count=0, + ), + then=expressions.Value(enums.OutgoingTransferableState.PENDING), + ), + default=expressions.Value(enums.OutgoingTransferableState.ONGOING), + output_field=models.CharField( + max_length=8, choices=enums.OutgoingTransferableState + ), + ) + + +def _build_transfer_finished_at_annotation() -> expressions.Case: + """Build the annotation for computing the date at which the transfer through the + diode finished when querying the Transferable(s). + + This is done by finding the finish date for the last transferable range. + If the transferable is still being submitted, the date is set to None. + + Returns: + expressions.Case: the Django ORM conditions for computing the transfer finish + date + + """ + return expressions.Case( + expressions.When( + submission_succeeded_at__isnull=True, + then=None, + ), + expressions.When( + models.Q( + size=expressions.F("auto_bytes_transferred"), + submission_succeeded_at__isnull=False, + ), + then="auto_last_range_finished_at", + ), + default=None, + output_field=models.DateTimeField(null=True), + ) + + +def _build_progress_annotation() -> expressions.Case: + """Build the annotation for computing the progress when querying the Transferable(s). + + - if a transferable has not fully been submitted, progress is None + - if a fully submitted transferable has not yet seen any of its ranges transferred + then it is 0 + - if a fully submitted transferable is marked as success, then it is 100 + - else it is the percentage of ranges marked TRANSFERRED + + Returns: + expressions.Case: the Django ORM conditions for computing the transferable's + progress + + """ + return expressions.Case( + # handle cases when size is 0 and there is only one TransferableRange + expressions.When( + size=0, + then=F("auto_transferred_ranges_count") * 100, + ), + # we use nullif to prevent division by zero caused by early evaluations of SQL + # statements in specific conditions, see: + # https://www.postgresql.org/docs/9.0/sql-expressions.html#SYNTAX-EXPRESS-EVAL + default=expressions.F("auto_bytes_transferred") + * 100 + / functions.NullIf(expressions.F("size"), 0), + output_field=models.PositiveSmallIntegerField(null=True), + ) + + +def _build_transfer_duration_annotation() -> Epoch: + """Build query for annotating the `transfer_duration` in seconds to the + OutgoingTransferable. + + Returns: + Query for the number of seconds elapsed for this transfer. + + """ + return Epoch( + functions.Coalesce("transfer_finished_at", PythonNow()) + - expressions.F("created_at") + ) + + +def _build_transfer_speed_annotation() -> expressions.F: + """Build query for annotating the `transfer_speed` in Bytes/second to the + OutgoingTransferable. + + Returns: + Query for the speed of this transfer. + + """ + # we use nullif to prevent division by zero caused by early evaluations of SQL + # statements in specific conditions, see: + # https://www.postgresql.org/docs/9.0/sql-expressions.html#SYNTAX-EXPRESS-EVAL + return expressions.F("auto_bytes_transferred") / functions.NullIf( + expressions.F("transfer_duration"), 0 + ) + + +def _build_transfer_estimated_finish_date_annotation() -> expressions.Case: + """Build query for annotating the `transfer_finish_date` to + the OutgoingTransferable. + + Returns: + Query for computing the transfer's finish date from its TransferableRanges + + """ + return expressions.Case( + expressions.When(submission_succeeded_at__isnull=False, then=None), + # we use nullif to prevent division by zero caused by early evaluations of SQL + # statements in specific conditions, see: + # https://www.postgresql.org/docs/9.0/sql-expressions.html#SYNTAX-EXPRESS-EVAL + default=PythonNow() + + ConvertSecondsToDuration( + expressions.F("size") / functions.NullIf(expressions.F("transfer_speed"), 0) + ), + output_field=models.DateTimeField(null=True), + ) + + +class TransferableManager(models.Manager): + def get_queryset(self) -> query.QuerySet["OutgoingTransferable"]: + """Compute OutgoingTransferables' extra fields and annotate them to the QuerySet. + + Returns: + The queryset annotated with the computed fields. + + """ + return ( + super() + .get_queryset() + .annotate(state=_build_state_annotation()) + .annotate( + transfer_finished_at=_build_transfer_finished_at_annotation(), + ) + .annotate( + progress=_build_progress_annotation(), + transfer_duration=_build_transfer_duration_annotation(), + ) + .annotate( + transfer_speed=_build_transfer_speed_annotation(), + ) + .annotate( + transfer_estimated_finish_date=( + _build_transfer_estimated_finish_date_annotation() + ) + ) + ) + + +class TransferableManagerStateOnly(models.Manager): + def get_queryset(self) -> query.QuerySet["OutgoingTransferable"]: + """Compute OutgoingTransferables' state and annotate it to the QuerySet. + + Returns: + The queryset annotated with the state. + + """ + return super().get_queryset().annotate(state=_build_state_annotation()) + + +class OutgoingTransferable(common_models.AbstractBaseModel): + """A Transferable submitted or being submitted by a user on the origin side + i.e. a file to transfer to the destination side. + """ + + objects = TransferableManager() + + objects_with_state_only = TransferableManagerStateOnly() + + name = common_models.TransferableNameField( + verbose_name=_("Name"), + help_text=_("The name of the file corresponding to the Transferable"), + default="", + ) + sha1 = common_models.SHA1Field( + verbose_name=_("SHA-1"), + help_text=_("The SHA-1 digest of the file corresponding to the Transferable"), + null=True, + default=None, + ) + bytes_received = common_models.TransferableSizeField( + verbose_name=_("Amount of bytes received"), + help_text=_("The amount of bytes received until now for the Transferable"), + default=0, + ) + size = common_models.TransferableSizeField( + verbose_name=_("Size in bytes"), + help_text=_("The size in bytes of the file corresponding to the Transferable"), + null=True, + default=None, + ) + user_profile = models.ForeignKey( + "eurydice_origin_core.UserProfile", + on_delete=models.RESTRICT, + verbose_name=_("User profile"), + help_text=_("The profile of the user owning the Transferable"), + ) + user_provided_meta = common_models.UserProvidedMetaField( + verbose_name=_("User provided metadata"), + help_text=_("The metadata provided by the user on file submission"), + default=dict, + ) + submission_succeeded_at = models.DateTimeField( + null=True, + verbose_name=_("OutgoingTransferable's submission's success date"), + help_text=_( + "A timestamp indicating the date at which the " + "OutgoingTransferable's submission was successful" + ), + ) + auto_revocations_count = models.PositiveIntegerField( + editable=False, + default=0, + verbose_name=_("Associated revocations count (auto-field)"), + help_text=_( + "The total amount of TransferableRevocations associated to this " + "Transferable (this field is kept up-to-date via database triggers)" + ), + ) + auto_user_revocations_count = models.PositiveIntegerField( + editable=False, + default=0, + verbose_name=_("Associated CANCEL revocations count (auto-field)"), + help_text=_( + "The total amount of user cancelation TransferableRevocations associated " + "to this Transferable (this field is kept up-to-date via database triggers)" + ), + ) + auto_ranges_count = models.PositiveIntegerField( + editable=False, + default=0, + verbose_name=_("Associated ranges (auto-field)"), + help_text=_( + "The total amount of TransferableRanges associated to this Transferable " + "(this field is kept up-to-date via database triggers)" + ), + ) + auto_pending_ranges_count = models.PositiveIntegerField( + editable=False, + default=0, + verbose_name=_("Associated PENDING ranges (auto-field)"), + help_text=_( + "The total amount of TransferableRanges in PENDING state associated to " + "this Transferable (this field is kept up-to-date via database triggers)" + ), + ) + auto_transferred_ranges_count = models.PositiveIntegerField( + editable=False, + default=0, + verbose_name=_("Associated TRANSFERRED ranges (auto-field)"), + help_text=_( + "The total amount of TransferableRanges in TRANSFERRED state associated to " + "this Transferable (this field is kept up-to-date via database triggers)" + ), + ) + auto_canceled_ranges_count = models.PositiveIntegerField( + editable=False, + default=0, + verbose_name=_("Associated CANCELED ranges (auto-field)"), + help_text=_( + "The total amount of TransferableRanges in CANCELED state associated to " + "this Transferable (this field is kept up-to-date via database triggers)" + ), + ) + auto_error_ranges_count = models.PositiveIntegerField( + editable=False, + default=0, + verbose_name=_("Associated ERROR ranges (auto-field)"), + help_text=_( + "The total amount of TransferableRanges in ERROR state associated to " + "this Transferable (this field is kept up-to-date via database triggers)" + ), + ) + auto_last_range_finished_at = models.DateTimeField( + editable=False, + null=True, + verbose_name=_("Last associated range finish date (auto-field)"), + help_text=_( + "A timestamp indicating the end of the transfer of the last " + "TransferableRange associated to this Transferable (this field is kept " + "up-to-date via database triggers)" + ), + ) + auto_bytes_transferred = common_models.TransferableSizeField( + editable=False, + default=0, + verbose_name=_("Amount of bytes transferred (auto-field)"), + help_text=_( + "The amount of this Transferable's bytes that have already been " + "transferred (this field is kept up-to-date via database triggers)" + ), + ) + auto_state_updated_at = models.DateTimeField( + auto_now_add=True, + editable=False, + verbose_name=_("Last state update date (auto-field)"), + help_text=_( + "A timestamp indicating the last time the state of this Transferable " + "changed" + ), + ) + + @property + def submission_succeeded(self) -> bool: + """Check if submission succeeded for this Transferable. + + Returns: + True if the user submission for this Transferable is over, + False otherwise + + """ + return self.submission_succeeded_at is not None + + def save( + self, + force_insert: bool = False, + force_update: bool = False, + using: Optional[str] = None, + update_fields: Optional[Iterable[str]] = None, + ) -> None: + """Hook just before Django saves, to never update auto-fields.""" + + if not force_insert and update_fields is None: + update_fields = [ + field.name + for field in self.__class__._meta.concrete_fields + if not field.primary_key and not field.name.startswith("auto_") + ] + + return super().save(force_insert, force_update, using, update_fields) + + class Meta: + db_table = "eurydice_outgoing_transferables" + base_manager_name = "objects" + indexes = [ + models.Index( + fields=[ + "-created_at", + ] + ), + models.Index( + fields=[ + "-auto_state_updated_at", + ] + ), + ] + constraints = [ + models.CheckConstraint( + name="%(app_label)s_%(class)s_sha1", + check=Q(submission_succeeded_at__isnull=True, sha1__isnull=True) + | Q( + submission_succeeded_at__isnull=False, + sha1__isnull=False, + sha1__length=common_models.SHA1Field.DIGEST_SIZE_IN_BYTES, + ), + ), + models.CheckConstraint( + name="%(app_label)s_%(class)s_bytes_received", + check=Q(submission_succeeded_at__isnull=True) + | Q(bytes_received=F("size")), + ), + ] + + +__all__ = ("OutgoingTransferable",) diff --git a/backend/eurydice/origin/core/models/transferable_range.py b/backend/eurydice/origin/core/models/transferable_range.py new file mode 100644 index 0000000..f84b960 --- /dev/null +++ b/backend/eurydice/origin/core/models/transferable_range.py @@ -0,0 +1,180 @@ +from django.conf import settings +from django.core import validators +from django.db import models +from django.db.models import Q +from django.db.models.expressions import F +from django.utils import timezone +from django.utils.functional import cached_property +from django.utils.translation import gettext_lazy as _ + +import eurydice.common.models as common_models +from eurydice.origin.core import enums + + +class TransferableRange(common_models.AbstractBaseModel): + """A fragment of an OutgoingTransferable i.e. a chunk of a file submitted by + a user. + """ + + byte_offset = models.PositiveBigIntegerField( + validators=(validators.MaxValueValidator(settings.TRANSFERABLE_MAX_SIZE),), + verbose_name=_("Byte offset"), + help_text=_("The start position of this range in the associated Transferable"), + ) + size = models.PositiveIntegerField( + validators=(validators.MaxValueValidator(settings.TRANSFERABLE_RANGE_SIZE),), + verbose_name=_("Size in bytes"), + help_text=_("The size in bytes of this TransferableRange"), + ) + s3_bucket_name = common_models.S3BucketNameField( + verbose_name=_("S3 bucket name"), + help_text=_( + "The name of the S3 bucket containing the file chunk " + "corresponding to this TransferableRange" + ), + ) + s3_object_name = common_models.S3ObjectNameField( + verbose_name=_("S3 object name"), + help_text=_( + "The name of the S3 object holding the data " + "corresponding to this TransferableRange" + ), + ) + transfer_state = models.CharField( + max_length=11, + choices=enums.TransferableRangeTransferState.choices, + default=enums.TransferableRangeTransferState.PENDING, + verbose_name=_("Transfer state"), + help_text=_("The state of the transfer for this OutgoingTransferable"), + ) + finished_at = models.DateTimeField( + null=True, + verbose_name=_("Transfer finish date"), + help_text=_( + "A timestamp indicating the end of the transfer of the Transferable" + ), + ) + outgoing_transferable = models.ForeignKey( + "eurydice_origin_core.OutgoingTransferable", + on_delete=models.CASCADE, + related_name="transferable_ranges", + ) + + @cached_property + def is_last(self) -> bool: + """Assert this TransferableRange is the last of the Transferable. + + Returns: + bool: True if this TransferableRange is the last for the associated + Transferable, False otherwise + """ + if not self.outgoing_transferable.submission_succeeded: + return False + + if ( + self.outgoing_transferable.size is not None + and self.outgoing_transferable.size == self.size + ): + return True + + return ( + type(self) + ._default_manager.filter( + outgoing_transferable__id=self.outgoing_transferable.id # type: ignore + ) + .order_by("byte_offset") + .only("id") + .last() + .id + == self.id + ) + + def _mark_as_finished( + self, transfer_state: enums.TransferableRangeTransferState, save: bool + ) -> None: + """ + Mark the TransferableRange as `transfer_state` and set its finish date + to now. + + Args: + transfer_state: the TransferState the TransferableRange must be set to. + save: whether changed fields should be saved or not. + + """ + self.transfer_state = transfer_state + self.finished_at = timezone.now() + + if save: + self.save(update_fields=["transfer_state", "finished_at"]) + + def mark_as_transferred(self, save: bool = True) -> None: + """ + Mark the current TransferableRange as TRANSFERRED in the DB and set + its finish date to now. + """ + self._mark_as_finished( # pytype: disable=wrong-arg-types + enums.TransferableRangeTransferState.TRANSFERRED, save + ) + + def mark_as_canceled(self, save: bool = True) -> None: + """ + Mark the current TransferableRange as REVOKED in the DB and set + its finish date to now. + """ + self._mark_as_finished( # pytype: disable=wrong-arg-types + enums.TransferableRangeTransferState.CANCELED, save + ) + + def mark_as_error(self, save: bool = True) -> None: + """ + Mark the current TransferableRange as ERROR in the DB and set + its finish date to now. + """ + self._mark_as_finished( # pytype: disable=wrong-arg-types + enums.TransferableRangeTransferState.ERROR, save + ) + + class Meta: + db_table = "eurydice_transferable_ranges" + constraints = [ + models.UniqueConstraint( + name="%(app_label)s_%(class)s_s3_bucket_name_s3_object_name", + fields=["s3_bucket_name", "s3_object_name"], + ), + models.CheckConstraint( + name="%(app_label)s_%(class)s_finished_at_transfer_state", + check=( + Q( + finished_at__isnull=True, + transfer_state=enums.TransferableRangeTransferState.PENDING, + ) + | Q( + finished_at__isnull=False, + transfer_state__in=( + enums.TransferableRangeTransferState.ERROR, + enums.TransferableRangeTransferState.TRANSFERRED, + enums.TransferableRangeTransferState.CANCELED, + ), + ) + ), + ), + models.CheckConstraint( + name="%(app_label)s_%(class)s_finished_at_created_at", + check=Q(finished_at__gte=F("created_at")), + ), + ] + indexes = [ + models.Index( + F("outgoing_transferable_id"), + name="eurydice_pending_ranges", + condition=Q(transfer_state="PENDING"), + ), + models.Index( + F("outgoing_transferable_id"), + name="eurydice_error_ranges", + condition=Q(transfer_state="ERROR"), + ), + ] + + +__all__ = ("TransferableRange",) diff --git a/backend/eurydice/origin/core/models/transferable_revocation.py b/backend/eurydice/origin/core/models/transferable_revocation.py new file mode 100644 index 0000000..69632ce --- /dev/null +++ b/backend/eurydice/origin/core/models/transferable_revocation.py @@ -0,0 +1,68 @@ +from django.db import models +from django.db.models.expressions import F +from django.utils.translation import gettext_lazy as _ + +from eurydice.common import enums +from eurydice.common import models as common_models +from eurydice.origin.core import enums as origin_enums +from eurydice.origin.core import models as origin_models + + +class TransferableRevocationQuerySet(models.QuerySet): + def list_pending(self) -> models.QuerySet: + """List `PENDING` TransferableRevocations ordered by date. + + Returns: + A QuerySet for the `PENDING` TransferableRevocations. + + """ + return self.filter( + transfer_state=origin_enums.TransferableRevocationTransferState.PENDING, + ).order_by("created_at") + + +class TransferableRevocation(common_models.AbstractBaseModel): + """The revocation for a Transferable. + + Stored in the DB to be transferred through the diode in order + to revoke the Transferable on the destination API. + + Attributes: + outgoing_transferable: The transferable to be revoked + reason: The reason for the revocation + transfer_state: The state of the Revocation's transfer through the diode + + """ + + objects = TransferableRevocationQuerySet.as_manager() + + outgoing_transferable = models.OneToOneField( + origin_models.OutgoingTransferable, + on_delete=models.CASCADE, + related_name="revocation", + ) + reason = models.CharField( + max_length=20, + choices=enums.TransferableRevocationReason.choices, + verbose_name=_("Revocation reason"), + help_text=_("The reason for the OutgoingTransferable's revocation"), + ) + transfer_state = models.CharField( + max_length=11, + choices=origin_enums.TransferableRevocationTransferState.choices, + default=origin_enums.TransferableRevocationTransferState.PENDING, + verbose_name=_("State of the Revocation"), + help_text=_("The OutgoingTransferable's revocation state"), + ) + + class Meta: + db_table = "eurydice_transferable_revocations" + indexes = [ + models.Index( + F("outgoing_transferable_id"), + name="eurydice_revocations_fgn_key", + ) + ] + + +__all__ = ("TransferableRevocation",) diff --git a/backend/eurydice/origin/core/models/user.py b/backend/eurydice/origin/core/models/user.py new file mode 100644 index 0000000..2f13efc --- /dev/null +++ b/backend/eurydice/origin/core/models/user.py @@ -0,0 +1,28 @@ +from django.conf import settings +from django.db import models +from django.utils.translation import gettext_lazy as _ + +import eurydice.common.models as common_models + + +class User(common_models.AbstractUser): + """A user on the origin side.""" + + +class UserProfile(common_models.AbstractBaseModel): + """User profile metadata.""" + + user = models.OneToOneField( + settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="user_profile" + ) + priority = models.PositiveSmallIntegerField( + default=0, + verbose_name=_("Priority"), + help_text=_("The priority level of the related user for transmitting files"), + ) + + class Meta: + db_table = "eurydice_user_profiles" + + +__all__ = ("User", "UserProfile") diff --git a/backend/eurydice/origin/core/signals.py b/backend/eurydice/origin/core/signals.py new file mode 100644 index 0000000..0b5ae4a --- /dev/null +++ b/backend/eurydice/origin/core/signals.py @@ -0,0 +1,21 @@ +"""This module declares Django database signals""" + +from django.db.models.signals import post_save +from django.dispatch import receiver + +from eurydice.origin.core import models + + +@receiver(post_save, sender=models.User) +def create_user_profile(instance: models.User, created: bool, *args, **kwargs) -> None: + """Create a UserProfile for the created user + + Args: + instance (models.User): the created user instance + + """ + if created: + try: + instance.user_profile + except models.UserProfile.DoesNotExist: + models.UserProfile.objects.create(user=instance) diff --git a/backend/eurydice/origin/sender/__init__.py b/backend/eurydice/origin/sender/__init__.py new file mode 100644 index 0000000..089ebb3 --- /dev/null +++ b/backend/eurydice/origin/sender/__init__.py @@ -0,0 +1,8 @@ +""" +The sender operates as a service distinct from the origin API and is responsible for +listening to the database for new TransferableRanges and sending them to a Lidis +instance to be transferred through the diode. + +It is intended to be run by calling its main.py module, a `run-sender` command is +available in the Makefile and it is advised to use it. +""" diff --git a/backend/eurydice/origin/sender/__main__.py b/backend/eurydice/origin/sender/__main__.py new file mode 100644 index 0000000..f38a7ff --- /dev/null +++ b/backend/eurydice/origin/sender/__main__.py @@ -0,0 +1,13 @@ +if __name__ == "__main__": + import os + + import django + + os.environ.setdefault( + "DJANGO_SETTINGS_MODULE", "eurydice.origin.config.settings.base" + ) + django.setup() + + from eurydice.origin.sender import main + + main.run() diff --git a/backend/eurydice/origin/sender/main.py b/backend/eurydice/origin/sender/main.py new file mode 100644 index 0000000..cec8e6b --- /dev/null +++ b/backend/eurydice/origin/sender/main.py @@ -0,0 +1,87 @@ +import datetime +import logging +import time +from typing import Optional + +from django.conf import settings +from django.db import connections +from django.utils import timezone + +import eurydice.origin.sender.utils as sender_utils +from eurydice.common import protocol +from eurydice.common.utils import signals +from eurydice.origin.core.models import LastPacketSentAt +from eurydice.origin.sender import packet_generator +from eurydice.origin.sender import packet_sender + +logging.config.dictConfig(settings.LOGGING) # type: ignore +logger = logging.getLogger(__name__) + + +def _log_packet_stats(packet: protocol.OnTheWirePacket) -> None: + """ + Log the given packets' statistics + + Args: + packet: packet to log stats about + """ + if packet.is_empty(): + logger.info("Sending heartbeat") + else: + logger.info(f"Sending {packet}") + + +def _heartbeat_should_be_sent(last_packet_sent_at: Optional[datetime.datetime]) -> bool: + """ + Determine if a heartbeat should be send based on the previous packet's send date + + Args: + last_packet_sent_at: datetime at which last packet was sent + + Returns: + True if heartbeat should be sent, False otherwise + """ + # Always send a heartbeat upon starting up + if last_packet_sent_at is None: + return True + return timezone.now() >= last_packet_sent_at + datetime.timedelta( + seconds=settings.HEARTBEAT_SEND_EVERY + ) + + +def _loop() -> None: + """ + Loop indefinitely until interrupted, sending packets as they become available + along with heartbeats when no packets are sent. + """ + + sender_utils.check_configuration() + + generator = packet_generator.OnTheWirePacketGenerator() + keep_running = signals.BooleanCondition() + + with packet_sender.PacketSender() as sender: + logger.info("Ready to send OnTheWirePackets") + + while keep_running: + packet = generator.generate_next_packet() + + if not packet.is_empty() or _heartbeat_should_be_sent( + sender.last_packet_sent_at + ): + sender.send(packet) + LastPacketSentAt.update() + _log_packet_stats(packet) + else: + time.sleep(settings.SENDER_POLL_DATABASE_EVERY) + + +def run() -> None: # pragma: no cover + """Entrypoint for the sender.""" + try: + _loop() + finally: + connections.close_all() + + +__all__ = ("run",) diff --git a/backend/eurydice/origin/sender/packet_generator/__init__.py b/backend/eurydice/origin/sender/packet_generator/__init__.py new file mode 100644 index 0000000..0947788 --- /dev/null +++ b/backend/eurydice/origin/sender/packet_generator/__init__.py @@ -0,0 +1,3 @@ +from .generator import OnTheWirePacketGenerator + +__all__ = ("OnTheWirePacketGenerator",) diff --git a/backend/eurydice/origin/sender/packet_generator/fillers/__init__.py b/backend/eurydice/origin/sender/packet_generator/fillers/__init__.py new file mode 100644 index 0000000..f975270 --- /dev/null +++ b/backend/eurydice/origin/sender/packet_generator/fillers/__init__.py @@ -0,0 +1,11 @@ +from .history import OngoingHistoryFiller +from .transferable_range import FIFOTransferableRangeFiller +from .transferable_range import UserRotatingTransferableRangeFiller +from .transferable_revocation import TransferableRevocationFiller + +__all__ = ( + "FIFOTransferableRangeFiller", + "UserRotatingTransferableRangeFiller", + "TransferableRevocationFiller", + "OngoingHistoryFiller", +) diff --git a/backend/eurydice/origin/sender/packet_generator/fillers/base.py b/backend/eurydice/origin/sender/packet_generator/fillers/base.py new file mode 100644 index 0000000..d6cac57 --- /dev/null +++ b/backend/eurydice/origin/sender/packet_generator/fillers/base.py @@ -0,0 +1,23 @@ +import abc + +import eurydice.common.protocol as protocol + + +class OnTheWirePacketFiller(abc.ABC): + """ + Abstract class for OnTheWirePacketFillers. + """ + + @abc.abstractmethod + def fill(self, packet: protocol.OnTheWirePacket) -> None: + """ + Subclasses should implement this method to fill the given packet with their + data. + + Args: + packet: packet to fill. + """ + raise NotImplementedError + + +__all__ = ("OnTheWirePacketFiller",) diff --git a/backend/eurydice/origin/sender/packet_generator/fillers/history.py b/backend/eurydice/origin/sender/packet_generator/fillers/history.py new file mode 100644 index 0000000..9a5bdb5 --- /dev/null +++ b/backend/eurydice/origin/sender/packet_generator/fillers/history.py @@ -0,0 +1,22 @@ +from eurydice.common import protocol +from eurydice.origin.sender import transferable_history_creator as history_creator +from eurydice.origin.sender.packet_generator.fillers import base + + +class OngoingHistoryFiller(base.OnTheWirePacketFiller): + """Fill the given packet with a TransferableHistory""" + + def __init__(self) -> None: # pragma: no cover + self._history_creator = history_creator.TransferableHistoryCreator() + + def fill(self, packet: protocol.OnTheWirePacket) -> None: + """ + Fill the given packet with a TransferableHistory + + Args: + packet: packet to fill + """ + packet.history = self._history_creator.get_next_history() + + +__all__ = ("OngoingHistoryFiller",) diff --git a/backend/eurydice/origin/sender/packet_generator/fillers/transferable_range.py b/backend/eurydice/origin/sender/packet_generator/fillers/transferable_range.py new file mode 100644 index 0000000..42a3be7 --- /dev/null +++ b/backend/eurydice/origin/sender/packet_generator/fillers/transferable_range.py @@ -0,0 +1,364 @@ +import logging +from collections import defaultdict +from typing import Dict +from typing import Iterator +from typing import List +from typing import Optional + +from django.conf import settings +from django.db import models +from django.db.models.query import QuerySet +from minio.deleteobjects import DeleteObject +from minio.error import S3Error + +import eurydice.common.protocol as protocol +import eurydice.origin.core.models as origin_models +import eurydice.origin.sender.user_selector as user_selector +from eurydice.common import exceptions +from eurydice.common import minio +from eurydice.common.utils import orm +from eurydice.origin.core import enums as origin_enums +from eurydice.origin.sender.packet_generator.fillers import base + +logging.config.dictConfig(settings.LOGGING) # type: ignore +logger = logging.getLogger(__name__) + + +def _build_protocol_transferable( + transferable_range: origin_models.TransferableRange, +) -> protocol.Transferable: + """ + Given the django model for a TransferableRange and a user, build the + protocol's model for the associated Transferable + + Args: + transferable_range: the TransferableRange django model + + Returns: + protocol's model for the associated Transferable + """ + + sha1: Optional[bytes] + + if transferable_range.is_last: + sha1 = bytes(transferable_range.outgoing_transferable.sha1) + else: + sha1 = None + + return protocol.Transferable( + id=transferable_range.outgoing_transferable.id, + name=transferable_range.outgoing_transferable.name, + user_provided_meta=transferable_range.outgoing_transferable.user_provided_meta, # noqa: E501 + sha1=sha1, + size=transferable_range.outgoing_transferable.size, + user_profile_id=transferable_range.outgoing_transferable.user_profile.id, + ) + + +def _fetch_next_transferable_ranges_for_user( + user: origin_models.User, +) -> QuerySet[origin_models.TransferableRange]: + """ + Fetch the given user's next MAX_TRANSFERABLES_PER_PACKET pending + TransferableRanges. + + Args: + user: for filtering TransferableRanges + + Returns: + the user's pending TransferableRanges + + """ + + return orm.make_queryset_with_subquery_join( + queryset=origin_models.TransferableRange.objects.select_related( + "outgoing_transferable__revocation" + ) + .filter( + outgoing_transferable__user_profile=user.user_profile, + transfer_state=origin_enums.TransferableRangeTransferState.PENDING, + ) + .order_by("created_at"), + subquery=origin_models.TransferableRange.objects.values( + "outgoing_transferable_id" + ).filter( + transfer_state=origin_enums.TransferableRangeTransferState.ERROR, + ), + on=models.Q(outgoing_transferable_id=models.F("outgoing_transferable_id")), + select={"erroneous_outgoing_transferable_id": "outgoing_transferable_id"}, + )[: settings.MAX_TRANSFERABLES_PER_PACKET] + + +def _fetch_pending_transferable_ranges() -> QuerySet[origin_models.TransferableRange]: + """ + Fetch the next MAX_TRANSFERABLES_PER_PACKET pending TransferableRanges. + + Also pre-fetches revocations and ERROR ranges, to reduce the amount of db queries. + + Returns: + the pending TransferableRanges + + """ + + return orm.make_queryset_with_subquery_join( + queryset=origin_models.TransferableRange.objects.select_related( + "outgoing_transferable__revocation" + ) + .filter( + transfer_state=origin_enums.TransferableRangeTransferState.PENDING, + ) + .order_by("created_at"), + subquery=origin_models.TransferableRange.objects.values( + "outgoing_transferable_id" + ).filter( + transfer_state=origin_enums.TransferableRangeTransferState.ERROR, + ), + on=models.Q(outgoing_transferable_id=models.F("outgoing_transferable_id")), + select={"erroneous_outgoing_transferable_id": "outgoing_transferable_id"}, + )[: settings.MAX_TRANSFERABLES_PER_PACKET] + + +def _get_transferable_range_data( + transferable_range: origin_models.TransferableRange, +) -> bytes: + """ + Given a TransferableRange, fetch and return its data from minio. + + Args: + transferable_range: range for which to fetch data + + Returns: + TransferableRange data as bytes + + Raises: + S3ObjectNotFoundError: if object is not found in S3. + + """ + response = None + try: + response = minio.client.get_object( + bucket_name=transferable_range.s3_bucket_name, + object_name=transferable_range.s3_object_name, + ) + data = response.read() + except S3Error as e: + if e.code == "NoSuchKey": + raise exceptions.S3ObjectNotFoundError() from e + raise + finally: + if response: + response.close() + response.release_conn() + return data + + +class OTWPacketAlreadyHasTransferableRanges(ValueError): + """ + Exception raised when passing an OnTheWirePacket which already has + TransferableRanges to the TransferableRangeFiller. + """ + + +def __group_transferable_ranges_by_bucket( + transferable_ranges: List[origin_models.TransferableRange], +) -> Dict[str, List[origin_models.TransferableRange]]: + result = defaultdict(list) + for transferable_range in transferable_ranges: + result[transferable_range.s3_bucket_name].append( + transferable_range, + ) + return result + + +def _delete_objects_from_s3( + transferable_ranges: List[origin_models.TransferableRange], +) -> None: + """ + Delete TransferableRanges data from S3. + + Args: + transferable_ranges: List of TransferableRanges to delete data from S3 + """ + transferable_ranges_by_bucket = __group_transferable_ranges_by_bucket( + transferable_ranges, + ) + + for bucket_name, transferable_ranges in transferable_ranges_by_bucket.items(): + errors = minio.client.remove_objects( + bucket_name=bucket_name, + delete_object_list=iter( + DeleteObject(transferable_range.s3_object_name) + for transferable_range in transferable_ranges + ), + ) + for error in errors: + logger.error("Error occurred when deleting object", error) + + +def _add( + transferable_range: origin_models.TransferableRange, + packet: protocol.OnTheWirePacket, +) -> None: + """ + Add given TransferableRange to the given packet, mark it as TRANSFERRED + and delete its associated data from s3. + + Args: + transferable_range: django model for the TransferableRange to add + packet: Pydantic protocol packet to add TransferableRange to + + """ + protocol_transferable = _build_protocol_transferable(transferable_range) + + try: + protocol_transferable_range = protocol.TransferableRange( + transferable=protocol_transferable, + is_last=transferable_range.is_last, + byte_offset=transferable_range.byte_offset, + data=_get_transferable_range_data(transferable_range), + ) + except exceptions.S3ObjectNotFoundError: + transferable_range.mark_as_error() + logger.error( + "Couldn't retrieve S3 object " + f"for TransferableRange {transferable_range.id} " + f"for Transferable {transferable_range.outgoing_transferable.id}." + ) + raise + + logger.info( + f"Add TransferableRange {transferable_range.id} " + f"for Transferable {transferable_range.outgoing_transferable.id}." + ) + packet.transferable_ranges.append(protocol_transferable_range) + + transferable_range.mark_as_transferred() + + +def _cancel( + transferable_range: origin_models.TransferableRange, +) -> None: + """Mark the provided transferable_range as CANCELED and remove its data from the + storage. + """ + logger.info(f"Cancel Transferable {transferable_range.outgoing_transferable.id}.") + transferable_range.mark_as_canceled() + + +def _process( + transferable_range: origin_models.TransferableRange, + packet: protocol.OnTheWirePacket, +) -> int: + """Process a transferable range by either adding it to the packet or cancelling + it.""" + if ( + transferable_range.erroneous_outgoing_transferable_id is not None # type: ignore # noqa: E501 + or hasattr(transferable_range.outgoing_transferable, "revocation") + ): + _cancel(transferable_range) + return 0 + + _add(transferable_range, packet) + + return transferable_range.size + + +class TransferableRangeFiller(base.OnTheWirePacketFiller): + """ + Abstract filler. + + Fills the given packet with TransferableRange's data and metadata + by calling the `fill()` method + """ + + def _get_transferable_ranges_to_process( + self, + ) -> Iterator[origin_models.TransferableRange]: + """ + TransferableRange iterator + + Yields: + TransferableRanges queried from DB + """ + raise NotImplementedError() + + def fill(self, packet: protocol.OnTheWirePacket) -> None: + """Given an OnTheWirePacket, fill it with TransferableRanges up to + TRANSFERABLE_RANGE_SIZE bytes if it has no existing TransferableRanges. + + Args: + packet: packet to fill with TransferableRanges + + Raises: + OTWPacketAlreadyHasTransferableRanges: when passed a packet which already + has TransferableRanges. + + """ + if len(packet.transferable_ranges) > 0: + raise OTWPacketAlreadyHasTransferableRanges + + packet_size = 0 + + to_delete = [] + + for transferable_range in self._get_transferable_ranges_to_process(): + try: + packet_size += _process(transferable_range, packet) + to_delete.append(transferable_range) + except exceptions.S3ObjectNotFoundError: + pass + + if packet_size >= settings.TRANSFERABLE_RANGE_SIZE: + break + + _delete_objects_from_s3(to_delete) + + +class UserRotatingTransferableRangeFiller(TransferableRangeFiller): + """ + Fill the given packet with TransferableRange's data and metadata + by calling the `fill()` method + """ + + def __init__(self) -> None: + self._user_selector = user_selector.WeightedRoundRobinUserSelector() + super().__init__() + + def _get_transferable_ranges_to_process( + self, + ) -> Iterator[origin_models.TransferableRange]: + """ + TransferableRange iterator using the _user_selector + + Yields: + TransferableRanges queried from DB + """ + self._user_selector.start_round() + + while user := self._user_selector.get_next_user(): + yield from _fetch_next_transferable_ranges_for_user(user) + + +class FIFOTransferableRangeFiller(TransferableRangeFiller): + """ + Fill the given packet with TransferableRange's data and metadata + by calling the `fill()` method + """ + + def _get_transferable_ranges_to_process( + self, + ) -> Iterator[origin_models.TransferableRange]: + """ + TransferableRange iterator + + Yields: + TransferableRanges queried from DB + """ + return iter(_fetch_pending_transferable_ranges()) + + +__all__ = ( + "TransferableRangeFiller", + "UserRotatingTransferableRangeFiller", + "FIFOTransferableRangeFiller", +) diff --git a/backend/eurydice/origin/sender/packet_generator/fillers/transferable_revocation.py b/backend/eurydice/origin/sender/packet_generator/fillers/transferable_revocation.py new file mode 100644 index 0000000..f3eb365 --- /dev/null +++ b/backend/eurydice/origin/sender/packet_generator/fillers/transferable_revocation.py @@ -0,0 +1,57 @@ +from typing import List + +from eurydice.common import protocol +from eurydice.origin.core import enums +from eurydice.origin.core import models +from eurydice.origin.sender.packet_generator.fillers import base + + +def _create_packet_revocations() -> List[protocol.TransferableRevocation]: + """Query the database and return PENDING TransferableRevocations as + pydantic models, finally update the transfer_state for the queried + TransferableRevocation. + + Returns: + PENDING TransferableRevocations + + """ + revocations = [] + queried_revocations = models.TransferableRevocation.objects.select_related( # type: ignore # noqa: E501 + "outgoing_transferable__user_profile", "outgoing_transferable" + ).list_pending() + + for revocation in queried_revocations: + revocations.append( + protocol.TransferableRevocation( + transferable_id=revocation.outgoing_transferable.id, + user_profile_id=revocation.outgoing_transferable.user_profile.id, + transferable_name=revocation.outgoing_transferable.name, + transferable_sha1=( + None + if not revocation.outgoing_transferable.sha1 + else bytes(revocation.outgoing_transferable.sha1) + ), + reason=revocation.reason, + ) + ) + + queried_revocations.update( + transfer_state=enums.TransferableRangeTransferState.TRANSFERRED + ) + return revocations + + +class TransferableRevocationFiller(base.OnTheWirePacketFiller): + """PacketFiller used to fill packet with TransferableRevocations""" + + def fill(self, packet: protocol.OnTheWirePacket) -> None: + """Fill the given packet with PENDING TransferableRevocations. + + Args: + packet: the packet to fill. + + """ + packet.transferable_revocations = _create_packet_revocations() + + +__all__ = ("TransferableRevocationFiller",) diff --git a/backend/eurydice/origin/sender/packet_generator/generator.py b/backend/eurydice/origin/sender/packet_generator/generator.py new file mode 100644 index 0000000..1961a93 --- /dev/null +++ b/backend/eurydice/origin/sender/packet_generator/generator.py @@ -0,0 +1,40 @@ +from django.conf import settings + +import eurydice.common.protocol as protocol +import eurydice.origin.sender.packet_generator.fillers as fillers +from eurydice.origin.core.models import Maintenance + + +class OnTheWirePacketGenerator: + """ + Allows for the generation of filled OnTheWirePackets ready to be sent + through the diode. + """ + + def __init__(self) -> None: + range_filler_class = getattr(fillers, settings.SENDER_RANGE_FILLER_CLASS) + self._fillers = ( + range_filler_class(), + fillers.TransferableRevocationFiller(), + fillers.OngoingHistoryFiller(), + ) + + def generate_next_packet(self) -> protocol.OnTheWirePacket: + """ + Generate a new OnTheWirePacket and fill it with the fillers' content + + In maintenance mode, the returned packet is empty. + + Returns: + the generated OnTheWirePacket + """ + packet = protocol.OnTheWirePacket() # type: ignore + + if not Maintenance.is_maintenance(): + for filler in self._fillers: + filler.fill(packet) + + return packet + + +__all__ = ("OnTheWirePacketGenerator",) diff --git a/backend/eurydice/origin/sender/packet_sender.py b/backend/eurydice/origin/sender/packet_sender.py new file mode 100644 index 0000000..bc8ba80 --- /dev/null +++ b/backend/eurydice/origin/sender/packet_sender.py @@ -0,0 +1,133 @@ +import datetime +import logging +import queue +import socket +import threading +from types import TracebackType +from typing import Optional +from typing import Type + +from django.conf import settings +from django.utils import timezone + +from eurydice.common import protocol + +logger = logging.getLogger(__name__) + + +class SenderThreadNotRunningError(RuntimeError): + """Raised when the sender thread is expected to be running but is not.""" + + +def _send_through_socket(data: bytes) -> None: + address = (settings.LIDIS_HOST, settings.LIDIS_PORT) + with socket.create_connection(address) as conn: + logger.debug( + f"Start sending data to {settings.LIDIS_HOST}:{settings.LIDIS_PORT}." + ) + conn.sendall(data) + logger.debug("Data successfully sent.") + + +def _is_poison_pill(data: Optional[bytes]) -> bool: + """Tell whether the data inputted is a 'poison pill' i.e. a packet signaling that + the sender thread must stop. + + """ + return data is None + + +class _SenderThread(threading.Thread): + def __init__(self, sending_queue: queue.Queue): + super().__init__() + self._queue = sending_queue + + def run(self) -> None: + while True: + data = self._queue.get(block=True) + + if _is_poison_pill(data): + break + + try: + _send_through_socket(data) + except socket.error: + logger.exception("Failed to send data through the socket.") + + +class PacketSender: + """Serialize OnTheWirePackets and send them to a Lidi sender service through + a TCP socket using a sender thread. + + Attributes: + last_packet_sent_at: date at which last packet was sent, None if no packets + have been sent + + Example: + >>> with packet_sender.PacketSender() as s: + ... s.send(on_the_wire_packet) + + """ + + def __init__(self) -> None: + self._queue: queue.Queue = queue.Queue( + maxsize=settings.PACKET_SENDER_QUEUE_SIZE + ) + self._sender_thread = _SenderThread(self._queue) + self.last_packet_sent_at: Optional[datetime.datetime] = None + + def start(self) -> None: + """Start the PacketSender i.e. start the sender thread. + + A PacketSender cannot be stopped then restarted. A new object must be created. + + """ + self._sender_thread.start() + + def stop(self) -> None: + """Stop the PacketSender i.e. ask the sender thread to stop and wait for it to + stop. + + The sender thread will stop and this method will return when there is no more + data packet waiting to be sent in the queue. + + """ + self._send_poison_pill() + self._sender_thread.join() + + def send(self, packet: protocol.OnTheWirePacket) -> None: + """Submit a OnTheWirePacket for being sent by the PacketSender. + + Args: + packet: the OnTheWirePacket to send. + + Raises: + SenderThreadNotRunningError: if the sender thread is not running, either + because the PacketSender has not been started, has been stopped, + or because the sender thread encountered a problem at runtime. + + """ + if not self._sender_thread.is_alive(): + raise SenderThreadNotRunningError() + + self._queue.put(packet.to_bytes(), block=True) + self.last_packet_sent_at = timezone.now() + + def _send_poison_pill(self) -> None: + """Send a poison pill packet to ask the sender thread to stop.""" + self._queue.put(None, block=True) + + def __enter__(self) -> "PacketSender": + self.start() + return self + + def __exit__( + self, + exctype: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + exc_traceback: Optional[TracebackType], + ) -> None: + self.stop() + + +__all__ = ("PacketSender",) diff --git a/backend/eurydice/origin/sender/transferable_history_creator.py b/backend/eurydice/origin/sender/transferable_history_creator.py new file mode 100644 index 0000000..63204f1 --- /dev/null +++ b/backend/eurydice/origin/sender/transferable_history_creator.py @@ -0,0 +1,107 @@ +import datetime +import logging +from typing import List +from typing import Optional + +from django.conf import settings +from django.db.models.query import QuerySet +from django.utils import timezone + +import eurydice.common.enums as enums +import eurydice.common.protocol as protocol +import eurydice.origin.core.models as models + +logger = logging.getLogger(__name__) + + +def _build_history_entries( + outgoing_transferables: QuerySet[models.OutgoingTransferable], +) -> List[protocol.HistoryEntry]: + """ + Given an OutgoingTransferable queryset, create and return HistoryEntries + + Args: + outgoing_transferables: OutgoingTransferables queryset to build history entries + + Returns: + HistoryEntries for the given OutgoingTransferables + """ + return [ + protocol.HistoryEntry( + transferable_id=transferable.id, + user_profile_id=transferable.user_profile_id, + # state is annotated + state=transferable.state, # type: ignore + name=transferable.name, + sha1=None if not transferable.sha1 else bytes(transferable.sha1), + user_provided_meta=transferable.user_provided_meta, + ) + for transferable in outgoing_transferables + ] + + +class TransferableHistoryCreator: + """ + Allows for the generation of OutgoingTransferable Histories. + """ + + def __init__(self) -> None: + self._previous_history_generated_at: Optional[datetime.datetime] = None + self._send_every = datetime.timedelta( + seconds=settings.TRANSFERABLE_HISTORY_SEND_EVERY + ) + self._history_duration = datetime.timedelta( + seconds=settings.TRANSFERABLE_HISTORY_DURATION + ) + + def too_soon(self, now: datetime.datetime) -> bool: + """ + Given a timestamp, return True if it is too soon to generate another History, + False otherwise + + Args: + now: timestamp to evaluate + + Returns: + True if too soon, False otherwise + """ + return ( + self._previous_history_generated_at is not None + and now - self._previous_history_generated_at < self._send_every + ) + + def get_next_history(self) -> Optional[protocol.History]: + """ + Returns the next OutgoingTransferable history if enough time has + passed since the last one. + + Returns: + the built History or None if previous History is too recent + """ + + now = timezone.now() + + if self.too_soon(now): + return None + + min_updated_at_date = now - self._history_duration + + logger.debug("Start history generation.") + outgoing_transferables = ( + models.OutgoingTransferable.objects_with_state_only.filter( + state__in=enums.OutgoingTransferableState.get_final_states(), + auto_state_updated_at__gte=min_updated_at_date, + ).only("id", "user_profile_id", "name", "sha1") + ) + + history = protocol.History( + entries=_build_history_entries(outgoing_transferables) + ) + logger.debug("History successfully generated.") + + self._previous_history_generated_at = now + + return history + + +__all__ = ("TransferableHistoryCreator",) diff --git a/backend/eurydice/origin/sender/user_selector.py b/backend/eurydice/origin/sender/user_selector.py new file mode 100644 index 0000000..270b89c --- /dev/null +++ b/backend/eurydice/origin/sender/user_selector.py @@ -0,0 +1,113 @@ +from typing import List +from typing import Optional +from uuid import UUID + +import eurydice.origin.core.models as models +from eurydice.origin.core import enums + + +class WeightedRoundRobinUserSelector: + """ + A User selector that implements a Weighted Round Robin algorithm to select the + next user with PENDING TransferableRange(s) to send. + """ + + def __init__(self) -> None: + self._round_counter: int = 0 + self._current_user: Optional[models.User] = None + self._pending_users_in_round: List[UUID] = [] + + def start_round(self) -> None: + """ + Start a Weighted Round Robin Round. + + This will fetch the UUIDs of all users that have at least one pending + TransferableRange when the round starts, and put them in the internal + pending users list. + """ + + self._pending_users_in_round = sorted( + models.TransferableRange.objects.values_list( # type: ignore + "outgoing_transferable__user_profile__user_id", + flat=True, + ) + .filter(transfer_state=enums.TransferableRangeTransferState.PENDING) + .distinct("outgoing_transferable__user_profile_id") + ) + + def get_next_user(self) -> Optional[models.User]: + """ + Retrieve the next user using a Weighted Round Robin algorithm. + + It is important to note that a user can only be selected once per round, + (i.e. once per OnTheWirePacket), even with a high priority. + + Indeed, when the algorithm selects a user, all of this user's pending + TransferableRanges will be retrieved and put in the OnTheWirePacket. If + after this the OnTheWirePacket is still not full, it is not suitable + to select the same user again (actually it could even cause a denial of + service with accurately timed zero-length files). + + Returns: + The user selected by the weighted round robin algorithm. + """ + + if not self._pending_users_in_round: + self._reset_current_user() + elif self._current_user is None: + self._select_arbitrary_pending_user() + elif ( + self._round_counter < self._current_user.user_profile.priority + and self._current_user.id in self._pending_users_in_round + ): + self._reselect_current_user() + else: + self._select_next_pending_user() + + self._round_counter += 1 + return self._current_user + + def _reset_current_user(self) -> None: + """Set the current user to None.""" + + # counting None rounds serves no purpose but makes the code clearer + self._round_counter = 0 + self._current_user = None + + def _select_arbitrary_pending_user(self) -> None: + """Selects the user with pending TransferableRanges with the lowest UUID.""" + + self._round_counter = 0 + self._current_user = models.User.objects.select_related("user_profile").get( + id=self._pending_users_in_round.pop(0) + ) + + def _reselect_current_user(self) -> None: + """Increase current user's rounds count and remove them from current round.""" + + current_user_id: UUID = self._current_user.id # type: ignore + self._pending_users_in_round.remove(current_user_id) + + def _select_next_pending_user(self) -> None: + """Select the user that comes after the current one.""" + + self._round_counter = 0 + self._current_user = models.User.objects.select_related("user_profile").get( + id=self._select_next_pending_user_id() + ) + + self._pending_users_in_round.remove(self._current_user.id) + + def _select_next_pending_user_id(self) -> UUID: + """Select the user ID that comes right after the current one, sorted by ID.""" + + return next( + filter( + (lambda user_id: user_id > self._current_user.id), # type: ignore + self._pending_users_in_round, + ), + self._pending_users_in_round[0], + ) + + +__all__ = ("WeightedRoundRobinUserSelector",) diff --git a/backend/eurydice/origin/sender/utils.py b/backend/eurydice/origin/sender/utils.py new file mode 100644 index 0000000..86219c6 --- /dev/null +++ b/backend/eurydice/origin/sender/utils.py @@ -0,0 +1,17 @@ +import django.core.exceptions as django_exceptions +from django.conf import settings + + +def check_configuration() -> None: + """Verify LIDIS configuration + + Raises: + django_exceptions.ImproperlyConfigured: when LIDIS_HOST or PORT is missing + """ + if not all((settings.LIDIS_HOST, settings.LIDIS_PORT)): + raise django_exceptions.ImproperlyConfigured( + "Both LIDIS_HOST and LIDIS_PORT environment variables must be defined" + ) + + +__all__ = ("check_configuration",) diff --git a/backend/eurydice/templates/admin/change_list_with_date_pickers.html b/backend/eurydice/templates/admin/change_list_with_date_pickers.html new file mode 100644 index 0000000..3ffcdb7 --- /dev/null +++ b/backend/eurydice/templates/admin/change_list_with_date_pickers.html @@ -0,0 +1,65 @@ +{% extends "admin/change_list.html" %} +{% load i18n admin_list %} + +{% block filters %} + +{% if cl.has_filters %} +
+

{% trans 'Filter' %}

+ +

By creation date

+ +

+ From:

+ To:
+

+ +
+ + {% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %} +
+ + + +{% endif %} +{% endblock %} diff --git a/backend/eurydice/templates/admin/eurydice_destination_core/incomingtransferable/change_list.html b/backend/eurydice/templates/admin/eurydice_destination_core/incomingtransferable/change_list.html new file mode 100644 index 0000000..c78e5eb --- /dev/null +++ b/backend/eurydice/templates/admin/eurydice_destination_core/incomingtransferable/change_list.html @@ -0,0 +1 @@ +{% extends "admin/change_list_with_date_pickers.html" %} diff --git a/backend/eurydice/templates/admin/eurydice_origin_core/outgoingtransferable/change_list.html b/backend/eurydice/templates/admin/eurydice_origin_core/outgoingtransferable/change_list.html new file mode 100644 index 0000000..c78e5eb --- /dev/null +++ b/backend/eurydice/templates/admin/eurydice_origin_core/outgoingtransferable/change_list.html @@ -0,0 +1 @@ +{% extends "admin/change_list_with_date_pickers.html" %} diff --git a/backend/manage.py b/backend/manage.py new file mode 100755 index 0000000..3afdce5 --- /dev/null +++ b/backend/manage.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + DJANGO_ENV = os.environ.get("DJANGO_ENV", None) + EURYDICE_API = os.environ.get("EURYDICE_API", "origin") + if DJANGO_ENV is None: + settings_module = "base" + else: + settings_module = DJANGO_ENV.lower() + + DJANGO_SETTINGS_MODULE = ( + f"eurydice.{EURYDICE_API}.config.settings.{settings_module}" + ) + + os.environ.setdefault("DJANGO_SETTINGS_MODULE", DJANGO_SETTINGS_MODULE) + + print(f"Using '{DJANGO_SETTINGS_MODULE}' as DJANGO_SETTINGS_MODULE") + + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..14b49bd --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,170 @@ +[tool.isort] +# Python black compatibility +# https://black.readthedocs.io/en/stable/compatible_configs.html#isort +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true +line_length = 88 +# Custom configuration +force_single_line = true +default_section = "THIRDPARTY" +known_first_party = "eurydice" + +[tool.coverage.run] +branch = true +omit = [ + "**/__init__.py", + "**/apps.py", + "**/admin.py", + "**/urls.py", + "**/wsgi.py", + "**/settings/*", + "**/migrations/*" +] +data_file = ".report/.coverage" + +[tool.coverage.report] +# Regexes for lines to exclude from consideration +exclude_lines = [ + "pragma: no cover", + "raise NotImplementedError", + "if TYPE_CHECKING:", + "if __name__ == .__main__.:" +] + +[tool.coverage.xml] +output = ".report/cobertura-coverage.xml" + +[tool.flakeheaven] +max_line_length = 88 +exclude = ["*/*/migrations/*", "*/*/settings/*"] + +[tool.flakeheaven.plugins] +"flake8-*" = ["+*"] +flake8-annotations = [ + "+*", + # Missing type annotation for self in method + "-ANN101", + # Missing type annotation for *args + "-ANN002", + # Missing type annotation for **kwargs + "-ANN003", + # Missing type annotation for cls in classmethod + "-ANN102", + # Missing return type annotation for special method + "-ANN204", +] +flake8-docstrings = [ + "-*", + # Missing Docstrings + "+D1??", + # Quotes Issues + "+D3??", + # Missing docstring in public module + "-D100", + # Missing docstring in public package + "-D104", + # Missing docstring in magic method + "-D105", + # Missing docstring in __init__ + "-D107", +] +flake8-pytest-style = ["+*", "-PT012"] +pycodestyle = [ + "+*", + # Whitespace before ':' (for black compatibility) + "-E203", + # missing whitespace after ',', ';', or ':' (for black compatibility) + "-E231", + # line break before binary operator (for black compatibility) + "-W503", +] +pyflakes = ["+*"] +radon = ["+*"] +mccabe = ["+*"] +dlint = [ + "+*", + # insecure use of "hashlib" module (duplicate with bandit) + "-DUO130", +] +pep8-naming = ["+*"] + +[tool.flakeheaven.exceptions."tests/**"] +flake8-docstrings = ["-*"] +flake8-annotations = [ + "+*", + # Missing return type annotation for public function + "-ANN201", +] + +[tool.flakeheaven.exceptions."**/__init__.py"] +pyflakes = [ + "+*", + # imported but unused + "-F401", +] + +[tool.flakeheaven.exceptions."**/apps.py"] +flake8-docstrings = ["-*"] + +[tool.flakeheaven.exceptions."**/urls.py"] +flake8-docstrings = ["-*"] + +[tool.flakeheaven.exceptions."**/filters.py"] +flake8-docstrings = ["-D106"] + +[tool.flakeheaven.exceptions."**/serializers.py"] +flake8-docstrings = ["-*"] + +[tool.flakeheaven.exceptions."**/manage.py"] +flake8-docstrings = ["-*"] + +[tool.flakeheaven.exceptions."**/models.py"] +flake8-docstrings = ["-D106"] + +[tool.flakeheaven.exceptions."**/models/*.py"] +flake8-docstrings = ["-D106"] + +[tool.flakeheaven.exceptions."**/admin.py"] +flake8-docstrings = [ + "+*", + # Missing docstring in public class + "-D101", + # Missing docstring in public class + "-D102", + # Missing docstring in public function + "-D103", +] + +[tool.flakeheaven.exceptions."**/renderers.py"] +flake8-docstrings = [ + "+*", + # Missing docstring in public class + "-D101", + # Missing docstring in public method + "-D102", +] + +[tool.flakeheaven.exceptions."**/exceptions.py"] +flake8-docstrings = [ + # Missing docstring in public class + "-D101", +] + +[tool.pytype] +keep_going = true +exclude = [ + "tests/", + "**/settings/", +] +python_version = "3.10" + +[tool.pytest.ini_options] +filterwarnings = [ + # Transform all warnings into errors. + "error", + "ignore::DeprecationWarning", + "ignore::django.utils.deprecation.RemovedInDjango50Warning" +] diff --git a/backend/settings.py b/backend/settings.py new file mode 100644 index 0000000..5f7c3cf --- /dev/null +++ b/backend/settings.py @@ -0,0 +1,18 @@ +""" +Dynamically generated Django settings. +Workaround to use django-stubs with multiple django projects. +""" + +import importlib +import os + +# Import the module +mod = importlib.import_module(os.environ["DS"]) + +# Determine a list of names to copy to the current name space +names = getattr(mod, "__all__", [n for n in dir(mod) if not n.startswith("_")]) + +# Copy those names into the current name space +g = globals() +for name in names: + g[name] = getattr(mod, name) diff --git a/backend/setup.cfg b/backend/setup.cfg new file mode 100644 index 0000000..405ffc9 --- /dev/null +++ b/backend/setup.cfg @@ -0,0 +1,44 @@ +[mypy] +# pyproject.toml support progress: +# https://github.com/python/mypy/issues/5205 + +python_version = 3.10 + +check_untyped_defs = True +disallow_any_generics = False +disallow_untyped_calls = True +disallow_untyped_decorators = True +ignore_errors = False +ignore_missing_imports = True +implicit_reexport = False +strict_optional = True +strict_equality = True +no_implicit_optional = True +warn_unused_ignores = True +warn_redundant_casts = True +warn_unused_configs = True +warn_unreachable = True +warn_no_return = True + +plugins = + mypy_django_plugin.main, + mypy_drf_plugin.main + +[mypy.plugins.django-stubs] +django_settings_module = "settings" + +[mypy-*.*.settings.*] +ignore_errors = true + +[mypy-*.*.migrations.*] +ignore_errors = true + +[mypy-tests.*] +ignore_errors = true + +[bandit] +# pyproject.toml support progress: +# https://github.com/PyCQA/bandit/pull/401 + +targets = eurydice +exclude = tests diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/common/__init__.py b/backend/tests/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/common/integration/__init__.py b/backend/tests/common/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/common/integration/endpoints/__init__.py b/backend/tests/common/integration/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/common/integration/endpoints/login.py b/backend/tests/common/integration/endpoints/login.py new file mode 100644 index 0000000..57441e8 --- /dev/null +++ b/backend/tests/common/integration/endpoints/login.py @@ -0,0 +1,83 @@ +import base64 + +import django.urls +import humanfriendly +import pytest +from django.contrib.auth.models import AbstractUser +from django.contrib.sessions.models import Session +from django.test import override_settings +from faker import Faker +from rest_framework import test +from rest_framework.authentication import BasicAuthentication + +from eurydice.common.api.views import UserDetailsView +from tests.common.integration.factory.session import SessionFactory + + +@override_settings(SESSION_COOKIE_SECURE=True, CSRF_COOKIE_SECURE=True) +def user_login_sets_cookies_with_default_values(api_client: test.APIClient): + url = django.urls.reverse("user-login") + + response = api_client.get(url, HTTP_X_REMOTE_USER="billmurray") + + assert response.status_code == 204 + assert response.data is None + + csrf_cookie = response.cookies["eurydice_csrftoken"] + session_cookie = response.cookies["eurydice_sessionid"] + + assert csrf_cookie["samesite"] == session_cookie["samesite"] == "Strict" + assert csrf_cookie["secure"] == session_cookie["secure"] == True # noqa: E712 + + assert session_cookie["max-age"] == humanfriendly.parse_timespan("24h") + + +def user_login_forbidden_without_remote_user_header(api_client: test.APIClient): + url = django.urls.reverse("user-login") + + response = api_client.get(url) + + assert response.status_code == 403 + assert len(response.cookies) == 0 + assert response.data["detail"].code == "not_authenticated" + + +def user_login_basic_auth(api_client: test.APIClient, user: AbstractUser): + # can't modify drf authentication settings at runtime, so here's a workaround + UserDetailsView.authentication_classes = [BasicAuthentication] + + username = user.username + password = user.password + user.set_password(password) + user.save() + + url = django.urls.reverse("user-me") + response = api_client.get(url) + assert response.status_code == 401 + + valid_credentials = base64.b64encode( + bytes(f"{username}:{password}", "utf-8") + ).decode("utf-8") + + api_client.credentials(HTTP_AUTHORIZATION=f"Basic {valid_credentials}") + response = api_client.get( + url, + ) + assert response.status_code == 200 + assert response.data["username"] == username + + +def user_login_removes_expired_sessions(api_client: test.APIClient, faker: Faker): + expire_date = faker.date_time_this_decade( + before_now=True, tzinfo=django.utils.timezone.get_current_timezone() + ) + expired_session = SessionFactory.create(expire_date=expire_date) + + url = django.urls.reverse("user-login") + + response = api_client.get(url, HTTP_X_REMOTE_USER="billmurray") + + assert response.status_code == 204 + + with pytest.raises(Session.DoesNotExist): + Session.objects.get(session_key=expired_session.session_key) diff --git a/backend/tests/common/integration/endpoints/metadata.py b/backend/tests/common/integration/endpoints/metadata.py new file mode 100644 index 0000000..9626963 --- /dev/null +++ b/backend/tests/common/integration/endpoints/metadata.py @@ -0,0 +1,18 @@ +from django.urls import reverse +from rest_framework import status +from rest_framework import test + +from eurydice.common.config.settings.test import EURYDICE_CONTACT_FR +from eurydice.common.config.settings.test import UI_BADGE_COLOR +from eurydice.common.config.settings.test import UI_BADGE_CONTENT + + +def metadata(api_client: test.APIClient): + url = reverse("server-metadata") + + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK, response.status_code + assert response.data["contact"] == EURYDICE_CONTACT_FR + assert response.data["badge_content"] == UI_BADGE_CONTENT + assert response.data["badge_color"] == UI_BADGE_COLOR diff --git a/backend/tests/common/integration/endpoints/pagination.py b/backend/tests/common/integration/endpoints/pagination.py new file mode 100644 index 0000000..35ec139 --- /dev/null +++ b/backend/tests/common/integration/endpoints/pagination.py @@ -0,0 +1,730 @@ +from typing import Any +from typing import Callable +from typing import List +from typing import Optional +from uuid import UUID + +import pytest +from django.db import transaction +from django.urls import reverse +from rest_framework import status +from rest_framework import test +from rest_framework.response import Response +from rest_framework.settings import api_settings + +from eurydice.common.api.pagination import EurydiceSessionPagination + +PAGES = 5 +PAGE_SIZE = 12 + + +class PaginationTestsInTransactionSuperclass(test.APITransactionTestCase): + def create_transferables(self) -> None: + """Initialize database with SUCCESSful and ERRORed transferables.""" + + assert PAGES >= 5 + assert PAGE_SIZE % 2 == 0 + assert PAGE_SIZE % 3 == 0 + assert PAGE_SIZE * PAGES > api_settings.PAGE_SIZE + + with transaction.atomic(): + self.user_profile = type(self).user_profile_factory() + type(self).make_transferables( + PAGES * PAGE_SIZE // 2, + user_profile=self.user_profile, + state=self.success_state, + ) + type(self).make_transferables( + PAGES * PAGE_SIZE // 2, + user_profile=self.user_profile, + state=self.success_state, + ) + + def list_transferables(self, **parameters) -> Response: + url = reverse("transferable-list") + + if "from_" in parameters: + parameters["from"] = parameters.pop("from_") + return self.client.get(url, parameters) + + def expected_ids(self) -> List[str]: + """Read the IDs we're going to paginate through, right from the database.""" + + return [ + str(elm) + for elm in type(self) + .transferable_class.objects.order_by("-created_at") + .values_list("id", flat=True) + ] + + def success_ids(self) -> List[str]: + """Read the IDs of Transferables in SUCCESS state.""" + + return [ + str(elm) + for elm in type(self) + .transferable_class.objects.filter(state=type(self).success_state.SUCCESS) + .order_by("-created_at") + .values_list("id", flat=True) + ] + + def trim_transferables(self, count: int) -> None: + """Delete the requested amount of transferables to emulate a DBTrimmer.""" + + type(self).transferable_class.objects.filter( + id__in=type(self) + .transferable_class.objects.order_by("created_at") + .values_list("id")[:count] + ).delete() + + def insert_decoy_transferables(self, count: int = 2) -> None: + """Add the requested amount of *new* transferables in the database.""" + + type(self).make_transferables( + count, + user_profile=type(self).transferable_class.objects.first().user_profile, + ) + + def assert_slice( + self, + response: Response, + expected_ids: List[str], + start: int, + end: int, + page: Optional[int] = None, + count: Optional[int] = None, + ): + """Make sure the API response matches the expected data slice.""" + + assert response.status_code == status.HTTP_200_OK + assert response.data["offset"] == start + assert response.data["count"] == len(expected_ids) if count is None else count + data = response.data["results"] + assert len(data) == end - start + for i in range(start, end): + assert data[i - start]["id"] == expected_ids[i] + + def setUp(self): + self.create_transferables() + self.client.force_authenticate(self.user_profile.user) + + def test_forward_ending( + self, + ): + TOTAL = PAGES * PAGE_SIZE # noqa: N806 + EXPECTED_LAST_PAGE_SIZE = 2 # noqa: N806 + TOTAL_WITHOUT_EXPECTED_LAST_PAGE = TOTAL - EXPECTED_LAST_PAGE_SIZE # noqa: N806 + TEST_PAGE_SIZE = int( # noqa: N806 + (TOTAL_WITHOUT_EXPECTED_LAST_PAGE) / (PAGES - 1) + ) + TEST_FULL_PAGES = int(TOTAL / TEST_PAGE_SIZE) # noqa: N806 + TEST_LAST_PAGE_SIZE = TOTAL - TEST_FULL_PAGES * TEST_PAGE_SIZE # noqa: N806 + + assert 0 < TEST_LAST_PAGE_SIZE < TEST_PAGE_SIZE + + expected_ids = self.expected_ids() + + response = self.list_transferables(page_size=TEST_PAGE_SIZE) + first = response.data["pages"]["current"] + response = self.list_transferables( + page_size=TEST_PAGE_SIZE, delta=TEST_FULL_PAGES, from_=first + ) + last_page = response.data["pages"]["current"] + assert response.data["pages"]["next"] is None + self.assert_slice( + response, + expected_ids, + TEST_PAGE_SIZE * TEST_FULL_PAGES, + TOTAL, + ) + + self.insert_decoy_transferables() + + response = self.list_transferables(page_size=TEST_PAGE_SIZE, page=last_page) + assert response.data["pages"]["next"] is None + self.assert_slice( + response, + expected_ids, + TEST_PAGE_SIZE * TEST_FULL_PAGES, + TOTAL, + ) + + response = self.list_transferables( + page_size=TEST_PAGE_SIZE, delta=1, from_=last_page + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_reverse_ending( + self, + ): + response = self.list_transferables(page_size=PAGE_SIZE) + assert response.data["pages"]["previous"] is None + current = response.data["pages"]["current"] + self.assert_slice(response, self.expected_ids(), 0, PAGE_SIZE) + + self.insert_decoy_transferables() + + response = self.list_transferables(page_size=PAGE_SIZE, delta=-1, from_=current) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_dbtrimmer_with_cursor_changed_pages( + self, + ): + expected_ids = self.expected_ids() + + response = self.list_transferables(page_size=PAGE_SIZE) + first_page = response.data["pages"]["current"] + response = self.list_transferables( + page_size=PAGE_SIZE, delta=PAGES - 1, from_=first_page + ) + current = response.data["pages"]["current"] + self.assert_slice( + response, expected_ids, PAGE_SIZE * (PAGES - 1), PAGE_SIZE * PAGES + ) + + self.trim_transferables(PAGE_SIZE // 2) + + response = self.list_transferables(page_size=PAGE_SIZE, page=current) + current = response.data["pages"]["current"] + self.assert_slice( + response, + expected_ids, + PAGE_SIZE * (PAGES - 1), + PAGE_SIZE * (PAGES - 1) + PAGE_SIZE // 2, + count=PAGE_SIZE * (PAGES - 1) + PAGE_SIZE // 2, + ) + + self.trim_transferables(PAGE_SIZE // 2 + 2) + + self.insert_decoy_transferables(PAGE_SIZE // 3) + + # The last page disappeared + response = self.list_transferables(page_size=PAGE_SIZE, page=current) + assert response.status_code == status.HTTP_410_GONE + + # The second to last page is now the last one, with 2 transferables less + response = self.list_transferables(page_size=PAGE_SIZE, delta=-1, from_=current) + self.assert_slice( + response, + expected_ids, + PAGE_SIZE * (PAGES - 2), + PAGE_SIZE * (PAGES - 1) - 2, + count=PAGE_SIZE * (PAGES - 1) - 2, + ) + + def test_almost_empty_database( + self, + ): + expected_ids = self.expected_ids() + + self.trim_transferables(PAGE_SIZE * PAGES - PAGE_SIZE // 2) + + response = self.list_transferables(page_size=PAGE_SIZE) + current = response.data["pages"]["current"] + self.assert_slice( + response, expected_ids, 0, PAGE_SIZE // 2, count=PAGE_SIZE // 2 + ) + + response = self.list_transferables(page_size=PAGE_SIZE, page=current) + self.assert_slice( + response, expected_ids, 0, PAGE_SIZE // 2, count=PAGE_SIZE // 2 + ) + + self.insert_decoy_transferables(2) + self.trim_transferables(PAGE_SIZE // 2) # Only 2 decoy transferables remaining + + response = self.list_transferables(page_size=PAGE_SIZE, page=current) + assert response.status_code == status.HTTP_410_GONE + + response = self.list_transferables(page_size=PAGE_SIZE) + current = response.data["pages"]["current"] + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 2 + + self.insert_decoy_transferables(2) + + response = self.list_transferables(page_size=PAGE_SIZE, page=current) + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 2 + + def test_empty_database( + self, + ): + expected_ids = self.expected_ids() + self.trim_transferables(PAGE_SIZE * PAGES - PAGE_SIZE // 2) + + response = self.list_transferables(page_size=PAGE_SIZE) + assert response.data["pages"]["previous"] is None + current = response.data["pages"]["current"] + self.assert_slice( + response, expected_ids, 0, PAGE_SIZE // 2, count=PAGE_SIZE // 2 + ) + + self.trim_transferables(PAGE_SIZE // 2) # Nothing left in the DB + + response = self.list_transferables(page_size=PAGE_SIZE) + assert response.data["pages"]["previous"] is None + assert response.data["pages"]["current"] is None + assert response.data["pages"]["next"] is None + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 0 + assert len(response.data["results"]) == 0 + + response = self.list_transferables(page_size=PAGE_SIZE, page=current) + assert response.status_code == status.HTTP_410_GONE + + def test_empty_database_edge_cases( + self, + ): + expected_ids = self.expected_ids() + self.trim_transferables(PAGE_SIZE * (PAGES - 1) - 1) + + response = self.list_transferables(page_size=PAGE_SIZE) + next_page = response.data["pages"]["next"] + response = self.list_transferables(page_size=PAGE_SIZE, page=next_page) + previous = response.data["pages"]["previous"] + current = response.data["pages"]["current"] + self.assert_slice( + response, + expected_ids, + PAGE_SIZE, + PAGE_SIZE + 1, + count=PAGE_SIZE + 1, + ) + + self.trim_transferables( + PAGE_SIZE + ) # 1 transferable left in the DB after this line + + response = self.list_transferables(page_size=PAGE_SIZE, page=current) + assert response.status_code == status.HTTP_410_GONE + + self.insert_decoy_transferables(2) + + response = self.list_transferables(page_size=PAGE_SIZE, page=previous) + self.assert_slice(response, expected_ids, 0, 1, count=1) + + response = self.list_transferables(page_size=PAGE_SIZE, delta=-1, from_=current) + self.assert_slice(response, expected_ids, 0, 1, count=1) + + self.trim_transferables(1) # Only decoys in the DB + + response = self.list_transferables(page_size=PAGE_SIZE, page=previous) + assert response.status_code == status.HTTP_410_GONE + + self.trim_transferables(2) # Nothing left in the DB + + response = self.list_transferables(page_size=PAGE_SIZE, delta=-1, from_=current) + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_additional_query_params( + self, + ): + expected_ids = self.success_ids() + + response = self.list_transferables(page_size=PAGE_SIZE // 3, state="SUCCESS") + self.assert_slice(response, expected_ids, 0, PAGE_SIZE // 3) + current = response.data["pages"]["current"] + + response = self.list_transferables( + state="SUCCESS", page_size=PAGE_SIZE // 3, delta=1, from_=current + ) + self.assert_slice(response, expected_ids, PAGE_SIZE // 3, 2 * PAGE_SIZE // 3) + current = response.data["pages"]["current"] + + # Removing filters is not allowed + response = self.list_transferables(page_size=PAGE_SIZE // 3, page=current) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + # Changing page size on the fly is not allowed + response = self.list_transferables( + state="SUCCESS", page_size=PAGE_SIZE // 2, page=current + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_invalid_page(self): + response = self.list_transferables(page_size=PAGE_SIZE * 2, page="billmurray") + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_invalid_page_delta( + self, + ): + response = self.list_transferables(page_size=PAGE_SIZE) + current = response.data["pages"]["current"] + self.assert_slice(response, self.expected_ids(), 0, PAGE_SIZE) + + response = self.list_transferables(page_size=PAGE_SIZE, delta=0, page=current) + self.assert_slice(response, self.expected_ids(), 0, PAGE_SIZE) + + response = self.list_transferables( + page_size=PAGE_SIZE, delta="hey", page=current + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_both_page_and_from( + self, + ): + response = self.list_transferables(page_size=PAGE_SIZE) + next_page = response.data["pages"]["next"] + current = response.data["pages"]["current"] + self.assert_slice(response, self.expected_ids(), 0, PAGE_SIZE) + + response = self.list_transferables( + page_size=PAGE_SIZE, delta=1, page=next_page, from_=current + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_page_delta_without_from( + self, + ): + response = self.list_transferables(page_size=PAGE_SIZE, delta=2) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +class PaginationTestsSuperclass: + @pytest.fixture(scope="class") + def list_transferables(self, django_db_setup: None, django_db_blocker: Any) -> None: + """Initialize database and return a lambda to query it.""" + + assert PAGES >= 5 + assert PAGE_SIZE % 2 == 0 + assert PAGE_SIZE % 3 == 0 + assert PAGE_SIZE * PAGES > api_settings.PAGE_SIZE + + with django_db_blocker.unblock(), transaction.atomic(): + user_profile = type(self).user_profile_factory() + type(self).make_transferables( + PAGES * PAGE_SIZE // 2, + user_profile=user_profile, + state=self.success_state, + ) + type(self).make_transferables( + PAGES * PAGE_SIZE // 2, + user_profile=user_profile, + state=self.error_state, + ) + api_client = test.APIClient() + # don't use force_login here as it attempts to create a session in DB + # and the atomic transaction block prevents other db queries + api_client.force_authenticate(user_profile.user) + url = reverse("transferable-list") + + def list_func(**parameters): + if "from_" in parameters: + parameters["from"] = parameters.pop("from_") + return api_client.get(url, parameters) + + yield list_func + transaction.set_rollback(True) + + @pytest.fixture(scope="class") + def expected_ids(self, list_transferables: Callable) -> List[str]: + """Read the IDs we're going to paginate through, right from the database.""" + + return [ + str(elm) + for elm in type(self) + .transferable_class.objects.order_by("-created_at") + .values_list("id", flat=True) + ] + + @pytest.fixture(scope="class") + def success_ids(self, list_transferables: Callable) -> List[str]: + """Read the IDs of Transferables in SUCCESS state.""" + + return [ + str(elm) + for elm in type(self) + .transferable_class.objects.filter(state=type(self).success_state.SUCCESS) + .order_by("-created_at") + .values_list("id", flat=True) + ] + + def trim_transferables(self, count: int) -> None: + """Delete the requested amount of transferables to emulate a DBTrimmer.""" + + type(self).transferable_class.objects.filter( + id__in=type(self) + .transferable_class.objects.order_by("created_at") + .values_list("id")[:count] + ).delete() + + def insert_decoy_transferables(self, count: int = 2) -> None: + """Add the requested amount of *new* transferables in the database.""" + + type(self).make_transferables( + count, + user_profile=type(self).transferable_class.objects.first().user_profile, + ) + + def assert_slice( + self, + response: Response, + expected_ids: List[str], + start: int, + end: int, + page: Optional[int] = None, + count: Optional[int] = None, + ): + """Make sure the API response matches the expected data slice.""" + + assert response.status_code == status.HTTP_200_OK + assert response.data["offset"] == start + assert response.data["count"] == len(expected_ids) if count is None else count + data = response.data["results"] + assert len(data) == end - start + for i in range(start, end): + assert data[i - start]["id"] == expected_ids[i] + + @pytest.mark.django_db() + def test_page_size(self, list_transferables: Callable, expected_ids: List[UUID]): + response = list_transferables(page_size=PAGE_SIZE // 2) + self.assert_slice(response, expected_ids, 0, PAGE_SIZE // 2) + + response = list_transferables(page_size=PAGE_SIZE // 3) + next_page = response.data["pages"]["next"] + response = list_transferables(page_size=PAGE_SIZE // 3, page=next_page) + self.assert_slice(response, expected_ids, PAGE_SIZE // 3, 2 * PAGE_SIZE // 3) + + @pytest.mark.django_db() + def test_full_listing_without_delta( + self, list_transferables: Callable, expected_ids: List[UUID] + ): + response = list_transferables(page_size=PAGE_SIZE) + next_page = response.data["pages"]["next"] + paginated_at = response.data["paginated_at"] + + self.assert_slice(response, expected_ids, 0, PAGE_SIZE) + + for page in range(1, PAGES): + response = list_transferables(page_size=PAGE_SIZE, page=next_page) + next_page = response.data["pages"]["next"] + assert paginated_at == response.data["paginated_at"] + self.assert_slice( + response, expected_ids, page * PAGE_SIZE, (page + 1) * PAGE_SIZE + ) + + @pytest.mark.django_db() + def test_full_listing_with_delta( + self, list_transferables: Callable, expected_ids: List[UUID] + ): + response = list_transferables(page_size=PAGE_SIZE) + current_page = response.data["pages"]["current"] + self.assert_slice(response, expected_ids, 0, PAGE_SIZE) + + self.insert_decoy_transferables() + + for page in range(1, PAGES): + response = list_transferables( + page_size=PAGE_SIZE, from_=current_page, delta=1 + ) + current_page = response.data["pages"]["current"] + self.assert_slice( + response, expected_ids, page * PAGE_SIZE, (page + 1) * PAGE_SIZE + ) + + @pytest.mark.django_db() + def test_page_delta(self, list_transferables: Callable, expected_ids: List[UUID]): + response = list_transferables(page_size=PAGE_SIZE) + current = response.data["pages"]["current"] + self.assert_slice(response, expected_ids, 0, PAGE_SIZE) + + response = list_transferables(page_size=PAGE_SIZE, delta=1, from_=current) + current = response.data["pages"]["current"] + self.assert_slice(response, expected_ids, PAGE_SIZE * 1, PAGE_SIZE * 2) + + self.insert_decoy_transferables() + + response = list_transferables(page_size=PAGE_SIZE, delta=2, from_=current) + current = response.data["pages"]["current"] + self.assert_slice(response, expected_ids, PAGE_SIZE * 3, PAGE_SIZE * 4) + + response = list_transferables(page_size=PAGE_SIZE, delta=-2, from_=current) + next_page = response.data["pages"]["next"] + self.assert_slice(response, expected_ids, PAGE_SIZE * 1, PAGE_SIZE * 2) + + self.insert_decoy_transferables() + + response = list_transferables(page_size=PAGE_SIZE, delta=2, from_=next_page) + self.assert_slice(response, expected_ids, PAGE_SIZE * 4, PAGE_SIZE * 5) + + @pytest.mark.django_db() + def test_full_listing_with_cursor_and_page_delta_backwards( + self, list_transferables: Callable, expected_ids: List[UUID] + ): + response = list_transferables(page_size=PAGE_SIZE) + first = response.data["pages"]["current"] + response = list_transferables(page_size=PAGE_SIZE, delta=PAGES - 1, from_=first) + last_page = response.data["pages"]["current"] + + response = list_transferables(page_size=PAGE_SIZE, page=last_page) + current = response.data["pages"]["current"] + self.assert_slice( + response, expected_ids, PAGE_SIZE * (PAGES - 1), PAGE_SIZE * PAGES + ) + + self.insert_decoy_transferables() + + for page in range(PAGES - 1, 0, -1): + response = list_transferables(page_size=PAGE_SIZE, delta=-1, from_=current) + current = response.data["pages"]["current"] + self.assert_slice( + response, expected_ids, (page - 1) * PAGE_SIZE, page * PAGE_SIZE + ) + + @pytest.mark.django_db() + def test_full_listing_with_cursor_and_absolute_page_backwards( + self, list_transferables: Callable, expected_ids: List[UUID] + ): + response = list_transferables(page_size=PAGE_SIZE) + first = response.data["pages"]["current"] + response = list_transferables(page_size=PAGE_SIZE, delta=PAGES - 1, from_=first) + last_page = response.data["pages"]["current"] + + response = list_transferables(page_size=PAGE_SIZE, page=last_page) + previous = response.data["pages"]["previous"] + self.assert_slice( + response, expected_ids, PAGE_SIZE * (PAGES - 1), PAGE_SIZE * PAGES + ) + + self.insert_decoy_transferables() + + for page in range(PAGES - 1, 0, -1): + response = list_transferables(page_size=PAGE_SIZE, page=previous) + previous = response.data["pages"]["previous"] + self.assert_slice( + response, expected_ids, (page - 1) * PAGE_SIZE, page * PAGE_SIZE + ) + + @pytest.mark.django_db() + @pytest.mark.parametrize("page", [1, int(PAGES / 2), PAGES]) + def test_refresh_with_current( + self, list_transferables: Callable, expected_ids: List[UUID], page: int + ): + response = list_transferables(page_size=PAGE_SIZE) + if page != 1: + first = response.data["pages"]["current"] + response = list_transferables( + page_size=PAGE_SIZE, delta=page - 1, from_=first + ) + selected_page = response.data["pages"]["current"] + + response = list_transferables(page_size=PAGE_SIZE, page=selected_page) + current = response.data["pages"]["current"] + self.assert_slice( + response, expected_ids, (page - 1) * PAGE_SIZE, page * PAGE_SIZE + ) + + self.insert_decoy_transferables() + + response = list_transferables(page_size=PAGE_SIZE, page=current) + self.assert_slice( + response, expected_ids, (page - 1) * PAGE_SIZE, page * PAGE_SIZE + ) + + @pytest.mark.django_db() + @pytest.mark.parametrize("page", [2, int(PAGES / 2) + 1, PAGES]) + def test_refresh_with_reverse_cursor( + self, list_transferables: Callable, expected_ids: List[UUID], page: int + ): + response = list_transferables(page_size=PAGE_SIZE) + first = response.data["pages"]["current"] + response = list_transferables(page_size=PAGE_SIZE, delta=page - 1, from_=first) + selected_page = response.data["pages"]["previous"] + + response = list_transferables(page_size=PAGE_SIZE, page=selected_page) + current = response.data["pages"]["current"] + self.assert_slice( + response, expected_ids, (page - 2) * PAGE_SIZE, (page - 1) * PAGE_SIZE + ) + + self.insert_decoy_transferables() + + response = list_transferables(page_size=PAGE_SIZE, page=current) + self.assert_slice( + response, expected_ids, (page - 2) * PAGE_SIZE, (page - 1) * PAGE_SIZE + ) + + @pytest.mark.django_db() + def test_dbtrimmer_with_cursor_unchanged_pages( + self, list_transferables: Callable, expected_ids: List[UUID] + ): + response = list_transferables(page_size=PAGE_SIZE) + first_page = response.data["pages"]["current"] + response = list_transferables( + page_size=PAGE_SIZE, delta=PAGES - 3, from_=first_page + ) + current = response.data["pages"]["current"] + self.assert_slice( + response, expected_ids, PAGE_SIZE * (PAGES - 3), PAGE_SIZE * (PAGES - 2) + ) + + self.trim_transferables(5 * PAGE_SIZE // 2) + + self.insert_decoy_transferables() + + response = list_transferables(page_size=PAGE_SIZE, page=current) + self.assert_slice( + response, + expected_ids, + PAGE_SIZE * (PAGES - 3), + PAGE_SIZE * (PAGES - 3) + PAGE_SIZE // 2, + count=PAGE_SIZE * (PAGES - 3) + PAGE_SIZE // 2, + ) + + @pytest.mark.django_db() + def test_max_page_size( + self, list_transferables: Callable, expected_ids: List[UUID] + ): + backup = EurydiceSessionPagination.max_page_size + EurydiceSessionPagination.max_page_size = 2 * PAGE_SIZE + + response = list_transferables(page_size=2 * PAGE_SIZE) + self.assert_slice(response, expected_ids, 0, 2 * PAGE_SIZE) + + response = list_transferables(page_size=2 * PAGE_SIZE + 1) + self.assert_slice(response, expected_ids, 0, 2 * PAGE_SIZE) + + EurydiceSessionPagination.max_page_size = backup + + @pytest.mark.django_db() + def test_default_page_size( + self, list_transferables: Callable, expected_ids: List[UUID] + ): + size = api_settings.PAGE_SIZE + + response = list_transferables() + self.assert_slice(response, expected_ids, 0, size) + + @pytest.mark.django_db() + def test_new_items(self, list_transferables: Callable, expected_ids: List[UUID]): + response = list_transferables(page_size=PAGE_SIZE) + next_page = response.data["pages"]["next"] + response = list_transferables(page_size=PAGE_SIZE, page=next_page) + self.assert_slice(response, expected_ids, PAGE_SIZE, 2 * PAGE_SIZE) + assert response.data["new_items"] is False + current = response.data["pages"]["current"] + next_page = response.data["pages"]["next"] + + response = list_transferables(page_size=PAGE_SIZE, page=current) + self.assert_slice(response, expected_ids, PAGE_SIZE, 2 * PAGE_SIZE) + assert response.data["new_items"] is False + + response = list_transferables(page_size=PAGE_SIZE, delta=1, from_=current) + self.assert_slice(response, expected_ids, PAGE_SIZE * 2, PAGE_SIZE * 3) + assert response.data["new_items"] is False + + self.insert_decoy_transferables(1) + + response = list_transferables(page_size=PAGE_SIZE, page=current) + + self.assert_slice(response, expected_ids, PAGE_SIZE, PAGE_SIZE * 2) + assert response.data["new_items"] is True + + response = list_transferables(page_size=PAGE_SIZE, delta=1, from_=current) + self.assert_slice(response, expected_ids, PAGE_SIZE * 2, PAGE_SIZE * 3) + assert response.data["new_items"] is True + + response = list_transferables(page_size=PAGE_SIZE, page=next_page) + self.assert_slice(response, expected_ids, PAGE_SIZE * 2, PAGE_SIZE * 3) + assert response.data["new_items"] is True diff --git a/backend/tests/common/integration/factory/__init__.py b/backend/tests/common/integration/factory/__init__.py new file mode 100644 index 0000000..72f1fd5 --- /dev/null +++ b/backend/tests/common/integration/factory/__init__.py @@ -0,0 +1,17 @@ +from .association import AssociationTokenFactory +from .protocol import HistoryEntryFactory +from .protocol import HistoryFactory +from .protocol import OnTheWirePacketFactory +from .protocol import TransferableFactory +from .protocol import TransferableRangeFactory +from .protocol import TransferableRevocationFactory + +__all__ = ( + "AssociationTokenFactory", + "TransferableFactory", + "TransferableRangeFactory", + "TransferableRevocationFactory", + "HistoryEntryFactory", + "HistoryFactory", + "OnTheWirePacketFactory", +) diff --git a/backend/tests/common/integration/factory/association.py b/backend/tests/common/integration/factory/association.py new file mode 100644 index 0000000..84a72b0 --- /dev/null +++ b/backend/tests/common/integration/factory/association.py @@ -0,0 +1,13 @@ +import factory + +from eurydice.common import association + + +class AssociationTokenFactory(factory.Factory): + user_profile_id = factory.Faker("uuid4", cast_to=None) + + class Meta: + model = association.AssociationToken + + +__all__ = ("AssociationTokenFactory",) diff --git a/backend/tests/common/integration/factory/protocol.py b/backend/tests/common/integration/factory/protocol.py new file mode 100644 index 0000000..2625113 --- /dev/null +++ b/backend/tests/common/integration/factory/protocol.py @@ -0,0 +1,91 @@ +import factory +from django.conf import settings + +from eurydice.common import enums +from eurydice.common import protocol + + +class TransferableFactory(factory.Factory): + id = factory.Faker("uuid4", cast_to=None) # noqa: VNE003 + name = factory.Faker("file_name") + user_profile_id = factory.Faker("uuid4", cast_to=None) + user_provided_meta = {"Meta-Foo": "bar", "Meta-Baz": "xyz"} + sha1 = factory.Faker("sha1", raw_output=True) + size = factory.Faker("pyint", min_value=0, max_value=settings.TRANSFERABLE_MAX_SIZE) + + class Meta: + model = protocol.Transferable + + +class TransferableRangeFactory(factory.Factory): + is_last = factory.Faker("pybool") + data = factory.Faker("binary", length=1024) + transferable = factory.SubFactory(TransferableFactory) + + _byte_offset = factory.Faker("pyint", max_value=settings.TRANSFERABLE_MAX_SIZE) + + @factory.lazy_attribute + def byte_offset(self) -> int: + return max(0, self._byte_offset - len(self.data)) + + class Meta: + model = protocol.TransferableRange + exclude = ("_byte_offset",) + + +class TransferableRevocationFactory(factory.Factory): + transferable_id = factory.Faker("uuid4", cast_to=None) + user_profile_id = factory.Faker("uuid4", cast_to=None) + reason = factory.Faker( + "random_element", elements=enums.TransferableRevocationReason + ) + transferable_name = factory.Faker("file_name") + transferable_sha1 = factory.Faker("sha1", raw_output=True) + + class Meta: + model = protocol.TransferableRevocation + + +class HistoryEntryFactory(factory.Factory): + transferable_id = factory.Faker("uuid4", cast_to=None) + user_profile_id = factory.Faker("uuid4", cast_to=None) + state = factory.Faker( + "random_element", + elements=enums.OutgoingTransferableState.get_final_states(), + ) + name = factory.Faker("file_name") + sha1 = factory.Faker("sha1", raw_output=True) + + class Meta: + model = protocol.HistoryEntry + + +class HistoryFactory(factory.Factory): + entries = factory.List([factory.SubFactory(HistoryEntryFactory) for _ in range(3)]) + + class Meta: + model = protocol.History + + +class OnTheWirePacketFactory(factory.Factory): + transferable_ranges = factory.List( + [factory.SubFactory(TransferableRangeFactory) for _ in range(3)] + ) + transferable_revocations = factory.List( + [factory.SubFactory(TransferableRevocationFactory) for _ in range(3)] + ) + history = factory.Maybe("_has_history", factory.SubFactory(HistoryFactory), None) + + class Meta: + model = protocol.OnTheWirePacket + exclude = ("_has_history",) + + +__all__ = ( + "TransferableFactory", + "TransferableRangeFactory", + "TransferableRevocationFactory", + "HistoryEntryFactory", + "HistoryFactory", + "OnTheWirePacketFactory", +) diff --git a/backend/tests/common/integration/factory/session.py b/backend/tests/common/integration/factory/session.py new file mode 100644 index 0000000..3957f2f --- /dev/null +++ b/backend/tests/common/integration/factory/session.py @@ -0,0 +1,14 @@ +import django.contrib.sessions.models +import factory + + +class SessionFactory(factory.django.DjangoModelFactory): + session_key = factory.Faker("pystr", min_chars=40, max_chars=40) + session_data = factory.Faker("bs") + expire_date = factory.Faker( + "date_time_this_decade", + tzinfo=django.utils.timezone.get_current_timezone(), + ) + + class Meta: + model = django.contrib.sessions.models.Session diff --git a/backend/tests/common/integration/utils/__init__.py b/backend/tests/common/integration/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/common/integration/utils/test_s3.py b/backend/tests/common/integration/utils/test_s3.py new file mode 100644 index 0000000..3c5a018 --- /dev/null +++ b/backend/tests/common/integration/utils/test_s3.py @@ -0,0 +1,79 @@ +import logging +from unittest import mock + +import pytest +from django import conf +from minio.error import S3Error + +from eurydice.common import minio +from eurydice.common.utils import s3 as s3_utils + + +def test_create_bucket_if_does_not_exist_success_create( + settings: conf.Settings, caplog: pytest.LogCaptureFixture +): + caplog.set_level(logging.INFO) + + bucket = "non-existing-bucket-x7859" + settings.MINIO_BUCKET_NAME = bucket + + s3_utils.create_bucket_if_does_not_exist() + minio.client.remove_bucket(bucket_name=bucket) + + assert caplog.messages == [f"Bucket '{bucket}' did not exist and was created"] + + +def test_create_bucket_if_does_not_exist_success_bucket_already_own( + settings: conf.Settings, caplog: pytest.LogCaptureFixture +): + caplog.set_level(logging.INFO) + + bucket = "non-existing-bucket-x5195" + settings.MINIO_BUCKET_NAME = bucket + + try: + minio.client.make_bucket(bucket_name=bucket) + + s3_utils.create_bucket_if_does_not_exist() + finally: + minio.client.remove_bucket(bucket_name=bucket) + + assert not caplog.messages + + +@mock.patch("eurydice.common.utils.s3.minio") +def test_create_bucket_raise_client_error( + minio: mock.Mock, caplog: pytest.LogCaptureFixture +): + response = {"Error": {"Code": "SomethingBadHappened"}} + minio.client.make_bucket.side_effect = S3Error( + code="SomethingBadHappened", + message="Oups", + resource="Resource", + request_id=42, + host_id=123, + response=response, + ) + + with pytest.raises(S3Error) as exc: + s3_utils.create_bucket_if_does_not_exist() + + assert exc.code == response["Error"]["Code"] + + assert not caplog.messages + minio.client.make_bucket.assert_called_once() + + +class SomeException(RuntimeError): + pass + + +@mock.patch("eurydice.common.utils.s3.minio") +def test_create_bucket_raise_error(minio: mock.Mock, caplog: pytest.LogCaptureFixture): + minio.client.make_bucket.side_effect = SomeException + + with pytest.raises(SomeException): + s3_utils.create_bucket_if_does_not_exist() + + assert not caplog.messages + minio.client.make_bucket.assert_called_once() diff --git a/backend/tests/common/integration/utils/test_signals.py b/backend/tests/common/integration/utils/test_signals.py new file mode 100644 index 0000000..cdde60e --- /dev/null +++ b/backend/tests/common/integration/utils/test_signals.py @@ -0,0 +1,51 @@ +import multiprocessing +import os +import signal +import time + +import pytest + +from eurydice.common.utils import signals + + +def _do_the_job( + shared_running: multiprocessing.Value, + ready_condition: multiprocessing.Condition, +) -> None: + keep_running = signals.BooleanCondition() + + with ready_condition: + ready_condition.notify() + + while keep_running: + time.sleep(0.1) + + shared_running.value = int(bool(keep_running)) + + +@pytest.mark.parametrize( + ("sig", "clean_exit"), + [(signal.SIGINT, True), (signal.SIGTERM, True), (signal.SIGKILL, False)], +) +def test_boolean_condition_success(sig: signal.Signals, clean_exit: bool): + ready_condition = multiprocessing.Condition() + shared_running = multiprocessing.Value("i", -1) + process = multiprocessing.Process( + target=_do_the_job, args=(shared_running, ready_condition) + ) + + process.start() + with ready_condition: + ready_condition.wait(timeout=5.0) + + os.kill(process.pid, sig) + + process.join() + + assert not process.is_alive() + if clean_exit: + assert process.exitcode == 0 + assert shared_running.value == 0 + else: + assert process.exitcode == -sig + assert shared_running.value == -1 diff --git a/backend/tests/common/unit/__init__.py b/backend/tests/common/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/common/unit/api/__init__.py b/backend/tests/common/unit/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/common/unit/api/docs/__init__.py b/backend/tests/common/unit/api/docs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/common/unit/api/docs/test_custom_spectacular.py b/backend/tests/common/unit/api/docs/test_custom_spectacular.py new file mode 100644 index 0000000..7591d3a --- /dev/null +++ b/backend/tests/common/unit/api/docs/test_custom_spectacular.py @@ -0,0 +1,92 @@ +from unittest import mock + +from drf_spectacular import utils + +from eurydice.common.api.docs import custom_spectacular + + +@mock.patch.object(utils, "extend_schema") +@mock.patch.object(custom_spectacular, "_code_samples", new_callable=dict) +def test_extend_schema(code_samples: dict, mocked_extend_schema: mock.Mock): + redoc_code_samples = [ + { + "lang": "Python", + "label": "Python 3", + "source": "import this", + } + ] + + custom_spectacular.extend_schema( + operation_id="test_op_id", + summary="heyy", + code_samples=redoc_code_samples, + ) + + mocked_extend_schema.assert_called_once_with( + operation_id="test_op_id", + summary="heyy", + ) + + assert code_samples["test_op_id"] == redoc_code_samples + + +@mock.patch.object(utils, "extend_schema") +@mock.patch.object(custom_spectacular, "_code_samples", new_callable=dict) +def test_extend_schema_no_samples(code_samples: dict, mocked_extend_schema: mock.Mock): + custom_spectacular.extend_schema( + operation_id="test_op_id", + summary="heyy", + ) + + mocked_extend_schema.assert_called_once_with( + operation_id="test_op_id", + summary="heyy", + ) + + assert "test_op_id" not in code_samples + + +@mock.patch.object(utils, "extend_schema") +@mock.patch.object(custom_spectacular, "_code_samples", new_callable=dict) +def test_postprocessing_hook(*_): + redoc_code_samples = [ + { + "lang": "Python", + "label": "Python 3", + "source": "import this", + } + ] + + custom_spectacular.extend_schema( + operation_id="test_op_id", + code_samples=redoc_code_samples, + ) + + openapi_json = { + "paths": { + "/transferables": { + "post": { + "operationId": "test_other_op_id", + "summary": "Bye", + }, + "get": { + "operationId": "test_op_id", + "summary": "Coucou", + }, + } + } + } + + openapi_json = custom_spectacular.postprocessing_hook( + result=openapi_json, generator=None, request=None, public=None + ) + + schema = openapi_json["paths"]["/transferables"]["get"] + + assert schema["summary"] == "Coucou" + assert schema["x-codeSamples"] == redoc_code_samples + + other_schema = openapi_json["paths"]["/transferables"]["post"] + + assert other_schema["summary"] == "Bye" + assert "x-codeSamples" not in other_schema diff --git a/backend/tests/common/unit/api/test_filters.py b/backend/tests/common/unit/api/test_filters.py new file mode 100644 index 0000000..7adb9cf --- /dev/null +++ b/backend/tests/common/unit/api/test_filters.py @@ -0,0 +1,14 @@ +from unittest import mock + +from faker import Faker + +from eurydice.common.api import filters + + +def test_filter_queryset_by_sha1(faker: Faker): + sha1_hex = faker.sha1() + sha1_bin = bytes.fromhex(sha1_hex) + + queryset = mock.Mock() + filters._filter_queryset_by_sha1(queryset, "sha1", sha1_hex) + queryset.filter.assert_called_once_with(sha1=sha1_bin) diff --git a/backend/tests/common/unit/api/test_serializers.py b/backend/tests/common/unit/api/test_serializers.py new file mode 100644 index 0000000..f2c498b --- /dev/null +++ b/backend/tests/common/unit/api/test_serializers.py @@ -0,0 +1,123 @@ +import collections + +import pytest +from django import conf +from django.utils import timezone +from faker import Faker +from rest_framework import serializers as drf_serializers + +from eurydice.common import association +from eurydice.common import bytes2words +from eurydice.common.api import serializers + + +class TestAssociationTokenSerializer: + def test_to_internal_value_success(self, faker: Faker): + obj = association.AssociationToken(user_profile_id=faker.uuid4(cast_to=None)) + deserialized_obj = serializers.AssociationTokenSerializer().to_internal_value( + {"token": bytes2words.encode(obj.to_bytes())} + ) + + assert isinstance(deserialized_obj, association.AssociationToken) + assert obj.user_profile_id == deserialized_obj.user_profile_id + assert obj.expires_at == deserialized_obj.expires_at + assert obj.digest == deserialized_obj.digest + + @pytest.mark.parametrize( + "value", + [ + bytes2words.encode( + b"\xddy\xba\xe1\x85\x8c\x0c6\xdai\xacw\x0clq" + b"\xe4\xb0\xf4\xc1\x0cs\xacf\t\rO\x07\xa0e3P\x80\xd4\xee" + ), + "abusif potatoe", + ], + ) + def test_to_internal_value_error_malformed_token(self, value: str): + with pytest.raises( + drf_serializers.ValidationError, + match="Malformed token.", + ): + serializers.AssociationTokenSerializer().to_internal_value({"token": value}) + + def test_to_internal_value_error_invalid_token_signature( + self, settings: conf.Settings, faker: Faker + ): + settings.USER_ASSOCIATION_TOKEN_SECRET_KEY = ( + "kzuzabvqkcc8b4frle16pptynbrlyo6pmfvx" + ) + forged_token_bytes = association.AssociationToken( + user_profile_id=faker.uuid4(cast_to=None) + ).to_bytes() + + settings.USER_ASSOCIATION_TOKEN_SECRET_KEY = ( + "14amnpyopw7f2xd8tok5tl8rr9jsnbti2vgx" + ) + + with pytest.raises( + drf_serializers.ValidationError, + match="Invalid association token signature.", + ): + serializers.AssociationTokenSerializer().to_internal_value( + {"token": bytes2words.encode(forged_token_bytes)} + ) + + def test_to_internal_value_error_expired_token(self, faker: Faker): + token = association.AssociationToken( + user_profile_id=faker.uuid4(cast_to=None), + expires_at=faker.date_time_this_decade( + tzinfo=timezone.get_current_timezone() + ), + ) + + with pytest.raises( + drf_serializers.ValidationError, match="The association token has expired." + ): + serializers.AssociationTokenSerializer().to_internal_value( + {"token": bytes2words.encode(token.to_bytes())} + ) + + def test_to_representation_success(self, faker: Faker): + token = association.AssociationToken(user_profile_id=faker.uuid4(cast_to=None)) + serialized = serializers.AssociationTokenSerializer().to_representation(token) + + assert isinstance(serialized, collections.abc.Mapping) + assert isinstance(serialized["token"], str) + assert isinstance(serialized["expires_at"], str) + + def test_to_representation_error_expired_token(self, faker: Faker): + token = association.AssociationToken( + user_profile_id=faker.uuid4(cast_to=None), + expires_at=faker.date_time_this_decade( + tzinfo=timezone.get_current_timezone() + ), + ) + + with pytest.raises(association.ExpiredToken): + serializers.AssociationTokenSerializer().to_representation(token) + + +@pytest.mark.parametrize( + ("representation", "value"), + [ + ( + "6c6120766172696174696f6e206475205049422064616e73206c65206d6f6e64652073" + "7569742074726573206578616374656d656e74206c6120766172696174696f6e206465" + "206c61207175616e7469746520646520706574726f6c652070726f6475697465", + b"la variation du PIB dans le monde suit tres exactement " + b"la variation de la quantite de petrole produite", + ), + ], +) +class TestBytesAsHexadecimalField: + def test_to_internal_value_success(self, representation: str, value: bytes): + assert ( + serializers.BytesAsHexadecimalField().to_internal_value(representation) + == value + ) + + def test_to_representation_success(self, representation: str, value: bytes): + assert ( + serializers.BytesAsHexadecimalField().to_representation(value) + == representation + ) diff --git a/backend/tests/common/unit/logging/test_formatter.py b/backend/tests/common/unit/logging/test_formatter.py new file mode 100644 index 0000000..cba28f5 --- /dev/null +++ b/backend/tests/common/unit/logging/test_formatter.py @@ -0,0 +1,67 @@ +import io +from logging import LogRecord + +import pytest +from django.core.handlers.wsgi import WSGIRequest + +from eurydice.common.logging import JSONFormatter + + +@pytest.mark.parametrize( + "value", + [ + LogRecord("name", 20, "path", 1, "message", None, None), + LogRecord( + "name", + 20, + "path", + 1, + "message %s", + ["args"], + {"exc_info": "exc_info"}, + "func", + "sinfo", + ), + LogRecord( + "name", + 20, + "path", + 1, + "message %s", + [{"deep": {"but": "still", "just": "a dict"}}], + {"exc_info": "exc_info"}, + "func", + "sinfo", + ), + LogRecord( + "name", + 20, + "path", + 1, + "message %s", + [WSGIRequest({"REQUEST_METHOD": "GET", "wsgi.input": io.BytesIO()})], + WSGIRequest({"REQUEST_METHOD": "GET", "wsgi.input": io.BytesIO()}), + "func", + "sinfo", + ), + LogRecord( + "name", + 20, + "path", + 1, + "message %s", + [Exception(Exception("test"))], + Exception(Exception("test")), + "func", + "sinfo", + ), + ], +) +def test_formatter_can_format(value: LogRecord): + assert JSONFormatter().format(value) is not None + + # Depending on the default formatter, value.message may not + # have been set. Test both with and without + # https://github.com/python/cpython/blob/26fa25a9a73f0e31bf0f0d94103fa4de38c0a3cc/Lib/logging/__init__.py#L678 # noqa: E501 + value.message = value.getMessage() + assert JSONFormatter().format(value) is not None diff --git a/backend/tests/common/unit/models/__init__.py b/backend/tests/common/unit/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/common/unit/models/test_fields.py b/backend/tests/common/unit/models/test_fields.py new file mode 100644 index 0000000..aee92d6 --- /dev/null +++ b/backend/tests/common/unit/models/test_fields.py @@ -0,0 +1,57 @@ +from typing import Dict + +import pytest +from django.conf import settings +from django.core import exceptions + +import eurydice.common.models.fields as fields + + +class TestUserProvidedMetaField: + @pytest.mark.parametrize( + ("field_value", "error_message"), + [ + ([], "Value must be a mapping."), + ({1: "foo"}, "Keys of the mapping must be strings."), + ( + {"": "foo"}, + f"Metadata item names must start with " + f"{settings.METADATA_HEADER_PREFIX}.", + ), + ( + {"Bar": "foo"}, + f"Metadata item names must start with " + f"{settings.METADATA_HEADER_PREFIX}.", + ), + ( + {f"{settings.METADATA_HEADER_PREFIX}Bar": 42}, + "Metadata item contents must be strings.", + ), + ( + { + f"{settings.METADATA_HEADER_PREFIX}Bar": "hello", + f"{settings.METADATA_HEADER_PREFIX}bar": "world", + }, + "Metadata item names are case insensitive and must not be duplicated.", + ), + ], + ) + def test_validate_error(self, field_value: Dict[str, str], error_message: str): + with pytest.raises(exceptions.ValidationError) as exc_info: # noqa: PT012 + fields.UserProvidedMetaField(blank=True).validate(field_value, None) + + assert exc_info.value.message == error_message + + @pytest.mark.parametrize( + "field_value", + [ + {}, + {f"{settings.METADATA_HEADER_PREFIX}Bar": "Baz"}, + { + f"{settings.METADATA_HEADER_PREFIX}Bar": "Baz", + f"{settings.METADATA_HEADER_PREFIX}Baz": "Xyz", + }, + ], + ) + def test_validate_success(self, field_value: Dict[str, str]): + fields.UserProvidedMetaField(blank=True).validate(field_value, None) diff --git a/backend/tests/common/unit/test_association.py b/backend/tests/common/unit/test_association.py new file mode 100644 index 0000000..9c1eac2 --- /dev/null +++ b/backend/tests/common/unit/test_association.py @@ -0,0 +1,77 @@ +import datetime + +import freezegun +import pytest +from django.conf import Settings +from django.utils import timezone +from faker import Faker + +from eurydice.common import association + + +class TestAssociationToken: + @pytest.fixture() + def token(self, faker: Faker) -> association.AssociationToken: + return association.AssociationToken(user_profile_id=faker.uuid4(cast_to=None)) + + def test_encode_decode_success(self, token: association.AssociationToken): + token_bytes = token.to_bytes() + token_from_bytes = association.AssociationToken.from_bytes(token_bytes) + + assert token_from_bytes.user_profile_id == token.user_profile_id + assert token_from_bytes.expires_at == token.expires_at + assert token_from_bytes.digest == token.digest + + @pytest.mark.parametrize("byte_length", [0, 35, 37]) + def test_decode_error_malformed(self, byte_length: int, faker: Faker): + with pytest.raises(association.MalformedToken): + association.AssociationToken.from_bytes(faker.binary(byte_length)) + + def test_decode_error_forged_signature(self, settings: Settings, faker: Faker): + settings.USER_ASSOCIATION_TOKEN_SECRET_KEY = ( + "kzuzabvqkcc8b4frle16pptynbrlyo6pmfvx" + ) + forged_token_bytes = association.AssociationToken( + user_profile_id=faker.uuid4(cast_to=None) + ).to_bytes() + + settings.USER_ASSOCIATION_TOKEN_SECRET_KEY = ( + "14amnpyopw7f2xd8tok5tl8rr9jsnbti2vgx" + ) + with pytest.raises(association.InvalidTokenDigest): + association.AssociationToken.from_bytes(forged_token_bytes) + + def test_decode_error_expired_token( + self, token: association.AssociationToken, faker: Faker + ) -> None: + token.expires_at = faker.date_time_this_decade( + tzinfo=timezone.get_current_timezone() + ) + token_bytes = token.to_bytes() + + with pytest.raises(association.ExpiredToken): + association.AssociationToken.from_bytes(token_bytes) + + @freezegun.freeze_time("2021-05-19 03:21:34.123456") + def test_set_expires_at_None_success( # noqa: N802 + self, settings: Settings, token: association.AssociationToken + ) -> None: + token._expires_at = None + + assert token.expires_at is None + + token.expires_at = None + assert token.expires_at == timezone.now().replace( + microsecond=0 + ) + datetime.timedelta(seconds=settings.USER_ASSOCIATION_TOKEN_EXPIRES_AFTER) + + @freezegun.freeze_time("2021-05-19 03:21:34.123456") + def test_set_expires_at_tz_aware_datetime_success( + self, token: association.AssociationToken + ) -> None: + token._expires_at = None + + assert token.expires_at is None + + token.expires_at = timezone.now() + assert token.expires_at == timezone.now().replace(microsecond=0) diff --git a/backend/tests/common/unit/test_bytes2words.py b/backend/tests/common/unit/test_bytes2words.py new file mode 100644 index 0000000..6c40a9c --- /dev/null +++ b/backend/tests/common/unit/test_bytes2words.py @@ -0,0 +1,59 @@ +import hashlib + +import pytest +from faker import Faker + +from eurydice.common import bytes2words + + +@pytest.mark.parametrize( + "value", + [ + hashlib.sha256((0).to_bytes(4, "big")).digest(), + hashlib.sha256((1).to_bytes(4, "big")).digest(), + hashlib.sha256((2).to_bytes(4, "big")).digest(), + hashlib.sha256((3).to_bytes(4, "big")).digest(), + hashlib.sha256((4).to_bytes(4, "big")).digest(), + b"\x00\x00\xff\xff", + b"\x00\x00\x00\x00", + ], +) +def test_encode_decode_success(value: bytes): + assert bytes2words.decode(bytes2words.encode(value)) == value + + +@pytest.mark.parametrize("length", [2, 4, 6, 8, 16, 32, 64, 66]) +def test_encode_decode_success_even_length(length: int): + value = bytes.fromhex("00" * length) + assert bytes2words.decode(bytes2words.encode(value)) == value + + +@pytest.mark.parametrize("length", [1, 3, 5, 99]) +def test_encode_error_odd_length(length: int): + value = bytes.fromhex("00" * length) + with pytest.raises(bytes2words.EncodingError, match="Data length must be even."): + bytes2words.encode(value) + + +@pytest.mark.parametrize("separator", [" ", " ", "-"]) +def test_decode_separator(separator: str, faker: Faker): + value = faker.binary(6) + token = bytes2words.encode(value) + + token_with_different_separator = token.replace(" ", separator) + + assert bytes2words.decode(token_with_different_separator) == value + + +@pytest.mark.parametrize( + "words", + [ + "abusif potatoe", + "Forasuccessfultechnologyrealitymusttakeprecedenceoverpublicrelations", + ], +) +def test_decode_error_unknown_word(words: str): + with pytest.raises( + bytes2words.DecodingError, match=r".*cannot be found in the index" + ): + bytes2words.decode(words) diff --git a/backend/tests/common/unit/test_protocol.py b/backend/tests/common/unit/test_protocol.py new file mode 100644 index 0000000..71a701c --- /dev/null +++ b/backend/tests/common/unit/test_protocol.py @@ -0,0 +1,177 @@ +import hashlib +from unittest import mock + +import humanfriendly as hf +import pydantic +import pytest +from faker import Faker + +from eurydice.common import enums +from eurydice.common import protocol + + +class TestHistoryEntry: + FINAL_STATES = enums.OutgoingTransferableState.get_final_states() + TRANSITORY_STATES = set(enums.OutgoingTransferableState) - FINAL_STATES + + @pytest.mark.parametrize("state", FINAL_STATES) + def test_validation_success( + self, state: enums.OutgoingTransferableState, faker: Faker + ): + protocol.HistoryEntry( + transferable_id=faker.uuid4(), + user_profile_id=faker.uuid4(), + state=state, + name="archive.zip", + sha1=hashlib.sha1(b"archive.zip").hexdigest(), + ) + + @pytest.mark.parametrize("state", TRANSITORY_STATES) + def test_validation_failure( + self, state: enums.OutgoingTransferableState, faker: Faker + ): + with pytest.raises(pydantic.ValidationError): + protocol.HistoryEntry( + transferable_id=faker.uuid4(), + user_profile_id=faker.uuid4(), + state=state, + name="archive.zip", + sha1=hashlib.sha1(b"archive.zip").hexdigest(), + ) + + +def test__pack_default_success_uuid(faker: Faker): + obj = faker.uuid4(cast_to=None) + assert protocol._pack_default(obj) == str(obj) + + +class TestOnTheWirePacket: + @pytest.mark.parametrize("nb_transferable_revocations", list(range(5))) + @pytest.mark.parametrize("nb_transferable_ranges", list(range(5))) + @pytest.mark.parametrize("has_history", [False, True]) + def test_encode_decode_success( + self, + has_history: bool, + nb_transferable_ranges: int, + nb_transferable_revocations: int, + faker: Faker, + ): + packet_to_send = protocol.OnTheWirePacket() + + if has_history: + packet_to_send.history = protocol.History( + entries=[ + protocol.HistoryEntry( + transferable_id=faker.uuid4(), + user_profile_id=faker.uuid4(), + state=enums.OutgoingTransferableState.SUCCESS, + name="archive.zip", + sha1=hashlib.sha1(b"archive.zip").hexdigest(), + ), + protocol.HistoryEntry( + transferable_id=faker.uuid4(), + user_profile_id=faker.uuid4(), + state=enums.OutgoingTransferableState.ERROR, + name="archive.zip", + sha1=hashlib.sha1(b"archive.zip").hexdigest(), + ), + ] + ) + + if nb_transferable_ranges > 0: + transferable = protocol.Transferable( + id=faker.uuid4(), + name=faker.file_name(), + user_profile_id=faker.uuid4(), + user_provided_meta={"Meta-Foo": "bar", "Meta-Baz": "xyz"}, + ) + + for idx in range(nb_transferable_ranges): + data = faker.binary(length=1024) + + transferable_range_is_last = idx + 1 == nb_transferable_ranges + if transferable_range_is_last: + transferable.sha1 = faker.sha1(raw_output=True) + transferable.size = faker.pyint( + min_value=0, max_value=500 + ) * hf.parse_size("1MB") + + packet_to_send.transferable_ranges.append( + protocol.TransferableRange( + transferable=transferable, + byte_offset=idx * len(data), + data=data, + is_last=transferable_range_is_last, + ) + ) + + for _ in range(nb_transferable_revocations): + packet_to_send.transferable_revocations.append( + protocol.TransferableRevocation( + transferable_id=faker.uuid4(cast_to=None), + user_profile_id=faker.uuid4(cast_to=None), + reason=enums.TransferableRevocationReason.USER_CANCELED, + transferable_name="archive.zip", + transferable_sha1=hashlib.sha1(b"archive.zip").hexdigest(), + ) + ) + + serialized = packet_to_send.to_bytes() + received_packet = protocol.OnTheWirePacket.from_bytes(serialized) + + assert received_packet == packet_to_send + + @mock.patch.object(pydantic.BaseModel, "dict") + def test_to_bytes_error_raises_SerializationError( # noqa: N802 + self, mocked_dict_func: mock.Mock + ): + mocked_dict_func.return_value = {"non_supported_object": object()} + packet = protocol.OnTheWirePacket() + + with pytest.raises(protocol.SerializationError): + packet.to_bytes() + + def test_from_bytes_error_raises_DeserializationError(self): # noqa: N802 + with pytest.raises(protocol.DeserializationError): + protocol.OnTheWirePacket.from_bytes(b"hello, world") + + +@pytest.mark.parametrize( + ("packet", "expected_emptiness"), + [ + # empty packet is empty + (protocol.OnTheWirePacket(), True), + # packet with empty history is not empty + (protocol.OnTheWirePacket(history=protocol.History(entries=[])), False), + # packet with at least one history entry is not empty + ( + protocol.OnTheWirePacket( + history=protocol.History( + entries=[mock.Mock(spec=protocol.HistoryEntry)] + ) + ), + False, + ), + # packet with at least one transferable range is not empty + ( + protocol.OnTheWirePacket( + transferable_ranges=[mock.Mock(spec=protocol.TransferableRange)] + ), + False, + ), + # packet with at least one transferable revocation is not empty + ( + protocol.OnTheWirePacket( + transferable_revocations=[ + mock.Mock(spec=protocol.TransferableRevocation) + ] + ), + False, + ), + ], +) +def test_on_the_wire_packet_is_empty( + packet: protocol.OnTheWirePacket, + expected_emptiness: bool, +): + assert packet.is_empty() is expected_emptiness diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 0000000..47caa69 --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,34 @@ +import factory.random +import pytest +from django.conf import settings +from rest_framework import test + + +@pytest.fixture(autouse=True, scope="session") +def _whitenoise_autorefresh() -> None: + """Get rid of whitenoise "No directory at" warning, + as it's not helpful when running tests. + + Related: + - https://github.com/evansd/whitenoise/issues/215 + - https://github.com/evansd/whitenoise/issues/191 + - https://github.com/evansd/whitenoise/commit/4204494d44213f7a51229de8bc224cf6d84c01eb # noqa: E501 + """ + settings.WHITENOISE_AUTOREFRESH = True + + +@pytest.fixture(autouse=True, scope="session") +def _setup_factory_boy() -> None: + """Set the default random seed value used by factory_boy.""" + factory.random.reseed_random(settings.FAKER_SEED) + + +@pytest.fixture(autouse=True, scope="session") +def faker_seed() -> int: + """Set the default random seed value used by Faker.""" + return settings.FAKER_SEED + + +@pytest.fixture() +def api_client() -> test.APIClient: + return test.APIClient() diff --git a/backend/tests/destination/__init__.py b/backend/tests/destination/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/integration/__init__.py b/backend/tests/destination/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/integration/api/__init__.py b/backend/tests/destination/integration/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/integration/api/test_http_error_format.py b/backend/tests/destination/integration/api/test_http_error_format.py new file mode 100644 index 0000000..c49ad36 --- /dev/null +++ b/backend/tests/destination/integration/api/test_http_error_format.py @@ -0,0 +1,33 @@ +from unittest import mock + +import django.urls +import pytest +from rest_framework import exceptions +from rest_framework import test + +from eurydice.destination.api.views.user_association import UserAssociationView +from tests.destination.integration import factory + + +@pytest.mark.django_db() +@pytest.mark.parametrize("exception", [exceptions.APIException, exceptions.ParseError]) +def test_association_token_error_is_json( + exception: exceptions.APIException, api_client: test.APIClient +): + """Tests that APIExceptions for HTTP errors 500 and 400 correctly return + a JSON formatted error. + + NOTE: we test this only for this specific endpoint as it should behave the + same regardless of which endpoint returns an HTTP error 500 and 400 + """ + user_profile = factory.UserProfileFactory() + + api_client.force_login(user=user_profile.user) + + url = django.urls.reverse("user-association") + + with mock.patch.object(UserAssociationView, "post", side_effect=exception): + response = api_client.post(url) + + assert response.status_code == exception.status_code + assert response.headers["Content-Type"] == "application/json" diff --git a/backend/tests/destination/integration/api/test_metadata.py b/backend/tests/destination/integration/api/test_metadata.py new file mode 100644 index 0000000..176b480 --- /dev/null +++ b/backend/tests/destination/integration/api/test_metadata.py @@ -0,0 +1,9 @@ +import pytest +from rest_framework import test + +from tests.common.integration.endpoints import metadata + + +@pytest.mark.django_db() +def test_metadata(api_client: test.APIClient): + metadata.metadata(api_client) diff --git a/backend/tests/destination/integration/api/test_permissions.py b/backend/tests/destination/integration/api/test_permissions.py new file mode 100644 index 0000000..a1d9856 --- /dev/null +++ b/backend/tests/destination/integration/api/test_permissions.py @@ -0,0 +1,30 @@ +from unittest import mock + +import pytest +from django import http +from faker import Faker + +from eurydice.common.api import permissions +from tests.destination.integration import factory + + +@pytest.mark.django_db() +class TestIsTransferableOwner: + def test_has_object_permission_authorized(self): + obj = factory.IncomingTransferableFactory() + request = mock.Mock() + request.user.id = obj.user_profile.user.id + + assert permissions.IsTransferableOwner.has_object_permission( + self=None, request=request, view=None, obj=obj + ) + + def test_has_object_permission_unauthorized(self, faker: Faker): + obj = factory.IncomingTransferableFactory() + request = mock.Mock() + request.user.id = faker.uuid4(cast_to=None) + + with pytest.raises(http.Http404): + permissions.IsTransferableOwner.has_object_permission( + self=None, request=request, view=None, obj=obj + ) diff --git a/backend/tests/destination/integration/api/test_responses.py b/backend/tests/destination/integration/api/test_responses.py new file mode 100644 index 0000000..e35b4e4 --- /dev/null +++ b/backend/tests/destination/integration/api/test_responses.py @@ -0,0 +1,65 @@ +import io +from typing import Dict +from typing import Optional +from unittest import mock + +import faker +import pytest +from urllib3._collections import HTTPHeaderDict +from urllib3.response import HTTPResponse + +from eurydice.destination.api import responses + + +class TestForwardedS3FileResponse: + @pytest.mark.parametrize("input_filename", [None, "file.ext", "file", ""]) + @pytest.mark.parametrize( + "extra_headers", [{}, {"Foo": "Bar"}, {"Foo": "Bar", "Baz": "Xyz"}] + ) + @pytest.mark.parametrize( + "data", [b"", b"Lorem ipsum dolor sit amet, consectetur adipiscing elit."] + ) + @mock.patch("eurydice.common.minio.client") + def test_create_and_read_success( + self, + minio_client: mock.Mock, + data: bytes, + extra_headers: Dict[str, str], + input_filename: Optional[str], + faker: faker.Faker, + ) -> None: + mock_http_response = mock.Mock(name="httplib.HTTPResponse") + headers = HTTPHeaderDict() + headers.add("Content-Length", f"{len(data)}") + s3_response = HTTPResponse( + body=io.BytesIO(data), + status=200, + preload_content=False, + headers=headers, + original_response=mock_http_response, + ) + minio_client.get_object.return_value = s3_response + + filename = input_filename or faker.file_name() + res = responses.ForwardedS3FileResponse( + bucket_name="foo", + object_name="bar", + filename=filename, + extra_headers=extra_headers, + ) + + expected_content_disposition = ( + "attachment" if not filename else f'attachment; filename="{filename}"' + ) + + assert res.headers == { + "Content-Length": f"{len(data)}", + "Content-Type": "application/octet-stream", + "Content-Disposition": expected_content_disposition, + **extra_headers, + } + + assert res.getvalue() == data + + with mock.patch("django.http.response.signals"): + res.close() diff --git a/backend/tests/destination/integration/api/test_username_http_header.py b/backend/tests/destination/integration/api/test_username_http_header.py new file mode 100644 index 0000000..07335c9 --- /dev/null +++ b/backend/tests/destination/integration/api/test_username_http_header.py @@ -0,0 +1,46 @@ +import django.urls +import pytest +from faker import Faker +from rest_framework import status +from rest_framework import test + +from tests.destination.integration import factory + + +@pytest.mark.django_db() +def test_http_header_with_authenticated_username_is_present_on_success( + api_client: test.APIClient, +): + user = factory.UserProfileFactory.create().user + url = django.urls.reverse("transferable-list") + api_client.force_login(user=user) + + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + assert response["Authenticated-User"] == user.username + + +@pytest.mark.django_db() +def test_http_header_with_authenticated_username_is_present_on_failure( + api_client: test.APIClient, faker: Faker +): + user = factory.UserProfileFactory.create().user + url = django.urls.reverse("transferable-detail", kwargs={"pk": faker.uuid4()}) + api_client.force_login(user=user) + + response = api_client.get(url) + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response["Authenticated-User"] == user.username + + +@pytest.mark.django_db() +def test_http_header_with_authenticated_username_is_not_present_when_not_authenticated( + api_client: test.APIClient, +): + url = django.urls.reverse("transferable-list") + response = api_client.get(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert not response.has_header("Authenticated-User") diff --git a/backend/tests/destination/integration/cleaning/__init__.py b/backend/tests/destination/integration/cleaning/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/integration/cleaning/dbtrimmer/__init__.py b/backend/tests/destination/integration/cleaning/dbtrimmer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/integration/cleaning/dbtrimmer/test_dbtrimmer.py b/backend/tests/destination/integration/cleaning/dbtrimmer/test_dbtrimmer.py new file mode 100644 index 0000000..6ffaada --- /dev/null +++ b/backend/tests/destination/integration/cleaning/dbtrimmer/test_dbtrimmer.py @@ -0,0 +1,36 @@ +import os +import subprocess +import sys + +import pytest +from django.conf import settings + +from eurydice.destination.cleaning import dbtrimmer + + +@pytest.mark.django_db() +def test_start_and_graceful_shutdown(): + with subprocess.Popen( + [sys.executable, "-m", dbtrimmer.__name__], + cwd=os.path.dirname(settings.BASE_DIR), + stderr=subprocess.PIPE, + env={ + "DB_NAME": settings.DATABASES["default"]["NAME"], + "DB_USER": settings.DATABASES["default"]["USER"], + "DB_PASSWORD": settings.DATABASES["default"]["PASSWORD"], + "DB_HOST": settings.DATABASES["default"]["HOST"], + "DB_PORT": str(settings.DATABASES["default"]["PORT"]), + "MINIO_ENDPOINT": settings.MINIO_ENDPOINT, + "MINIO_ACCESS_KEY": settings.MINIO_ACCESS_KEY, + "MINIO_SECRET_KEY": settings.MINIO_SECRET_KEY, + "MINIO_BUCKET_NAME": settings.MINIO_BUCKET_NAME, + "TRANSFERABLE_STORAGE_DIR": settings.TRANSFERABLE_STORAGE_DIR, + "USER_ASSOCIATION_TOKEN_SECRET_KEY": settings.USER_ASSOCIATION_TOKEN_SECRET_KEY, # noqa: E501 + }, + ) as proc: + while b"Ready" not in proc.stderr.readline(): + pass + + proc.terminate() + return_code = proc.wait() + assert return_code == 0 diff --git a/backend/tests/destination/integration/cleaning/dbtrimmer/test_main.py b/backend/tests/destination/integration/cleaning/dbtrimmer/test_main.py new file mode 100644 index 0000000..4337832 --- /dev/null +++ b/backend/tests/destination/integration/cleaning/dbtrimmer/test_main.py @@ -0,0 +1,237 @@ +import datetime +from typing import List +from unittest import mock + +import freezegun +import pytest +from django import conf +from django.utils import timezone +from faker import Faker + +import eurydice.destination.cleaning.dbtrimmer.dbtrimmer as dbtrimmer_module +from eurydice.common.utils import signals +from eurydice.destination.cleaning.dbtrimmer.dbtrimmer import DestinationDBTrimmer +from eurydice.destination.core import models +from tests.destination.integration import factory + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + ("transferable_states", "expected_deletions"), + [ + ([], 0), + ([models.IncomingTransferableState.ONGOING], 0), + ([models.IncomingTransferableState.SUCCESS], 0), + ([models.IncomingTransferableState.ERROR], 1), + ([models.IncomingTransferableState.REVOKED], 1), + ([models.IncomingTransferableState.EXPIRED], 1), + ([models.IncomingTransferableState.REMOVED], 1), + ( + [ + models.IncomingTransferableState.EXPIRED, + models.IncomingTransferableState.EXPIRED, + ], + 2, + ), + ( + [ + models.IncomingTransferableState.ONGOING, + models.IncomingTransferableState.SUCCESS, + models.IncomingTransferableState.ERROR, + models.IncomingTransferableState.REVOKED, + models.IncomingTransferableState.EXPIRED, + models.IncomingTransferableState.REMOVED, + ], + 4, + ), + ], +) +def test_dbtrimmer_by_transferable_states( + faker: Faker, + transferable_states: List[models.IncomingTransferableState], + expected_deletions: int, + caplog: pytest.LogCaptureFixture, + settings: conf.Settings, +): + settings.DBTRIMMER_TRIM_TRANSFERABLES_AFTER = datetime.timedelta(seconds=1) + + now = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + + for state in transferable_states: + created_at = ( + now + - settings.DBTRIMMER_TRIM_TRANSFERABLES_AFTER + - datetime.timedelta(seconds=1) + ) + + if state in models.IncomingTransferableState.get_final_states(): + finished_at = created_at + else: + finished_at = None + + factory.IncomingTransferableFactory( + state=state, + created_at=created_at, + finished_at=finished_at, + ) + + with freezegun.freeze_time(now): + DestinationDBTrimmer()._run() + + if expected_deletions == 0: + assert caplog.messages == [ + "DBTrimmer is running", + "DBTrimmer finished running", + ] + else: + assert caplog.messages == [ + "DBTrimmer is running", + f"DBTrimmer will remove {expected_deletions} entries.", + f"DBTrimmer successfully removed {expected_deletions} entries.", + "DBTrimmer finished running", + ] + + assert not models.IncomingTransferable.objects.filter( + state__in=( + models.IncomingTransferableState.ERROR, + models.IncomingTransferableState.REVOKED, + models.IncomingTransferableState.EXPIRED, + models.IncomingTransferableState.REMOVED, + ) + ).exists() + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + ("transferable_finished_at", "expected_deletions"), + [ + ([], 0), + ([datetime.timedelta(seconds=1)], 0), + ([datetime.timedelta(seconds=59)], 0), + ([datetime.timedelta(seconds=61)], 1), + ], +) +def test_dbtrimmer_by_transferable_finish_date( + faker: Faker, + transferable_finished_at: List[datetime.timedelta], + expected_deletions: int, + caplog: pytest.LogCaptureFixture, + settings: conf.Settings, +): + settings.DBTRIMMER_TRIM_TRANSFERABLES_AFTER = datetime.timedelta(seconds=60) + + now = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + + for delta_finished_at in transferable_finished_at: + finished_at = now - delta_finished_at + factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.EXPIRED, + created_at=finished_at, + finished_at=finished_at, + ) + + with freezegun.freeze_time(now): + DestinationDBTrimmer()._run() + + if expected_deletions == 0: + assert caplog.messages == [ + "DBTrimmer is running", + "DBTrimmer finished running", + ] + else: + assert caplog.messages == [ + "DBTrimmer is running", + f"DBTrimmer will remove {expected_deletions} entries.", + f"DBTrimmer successfully removed {expected_deletions} entries.", + "DBTrimmer finished running", + ] + + +@pytest.mark.django_db() +@mock.patch.object(signals.BooleanCondition, "__bool__") +@mock.patch("time.sleep") +def test_loop_success_no_transferable( + time_sleep: mock.Mock, + boolean_cond: mock.Mock, +): + boolean_cond.side_effect = [True, True, False] + dbtrimmer = DestinationDBTrimmer() + dbtrimmer._loop() + assert time_sleep.call_count == 2 + + +@mock.patch("eurydice.destination.core.models.IncomingTransferable.objects.filter") +@pytest.mark.django_db() +def test_deletion_count_mismatch_is_logged( + filter_mock: mock.Mock, + caplog: pytest.LogCaptureFixture, +): + sliced_qs = mock.Mock() + sliced_qs.values_list.return_value = ["xxx"] + qs = mock.MagicMock() + qs.__getitem__.return_value = sliced_qs + filter_mock.return_value = qs + filter_mock().delete.return_value = (0, None) + + DestinationDBTrimmer()._run() + + assert caplog.messages == [ + "DBTrimmer is running", + "DBTrimmer will remove 1 entries.", + "DBTrimmer successfully removed 0 entries.", + "DBTrimmer deleted 0 entries, instead of the expected 1.", + "DBTrimmer finished running", + ] + + +@pytest.mark.django_db() +def test_dbtrimmer_bulk_delete( + faker: Faker, + caplog: pytest.LogCaptureFixture, + settings: conf.Settings, +): + old_value = dbtrimmer_module.BULK_DELETION_SIZE + dbtrimmer_module.BULK_DELETION_SIZE = 1 + settings.DBTRIMMER_TRIM_TRANSFERABLES_AFTER = datetime.timedelta(seconds=1) + + now = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + + for state in [ + models.IncomingTransferableState.EXPIRED, + models.IncomingTransferableState.EXPIRED, + ]: + finished_at = ( + now + - settings.DBTRIMMER_TRIM_TRANSFERABLES_AFTER + - datetime.timedelta(seconds=1) + ) + + with freezegun.freeze_time(finished_at): + factory.IncomingTransferableFactory( + state=state, + created_at=finished_at, + finished_at=finished_at, + ) + + with freezegun.freeze_time(now): + DestinationDBTrimmer()._run() + + assert caplog.messages == [ + "DBTrimmer is running", + "DBTrimmer will remove 1 entries.", + "DBTrimmer successfully removed 1 entries.", + "DBTrimmer will remove 1 entries.", + "DBTrimmer successfully removed 1 entries.", + "DBTrimmer finished running", + ] + + assert not models.IncomingTransferable.objects.filter( + state__in=( + models.IncomingTransferableState.ERROR, + models.IncomingTransferableState.REVOKED, + models.IncomingTransferableState.EXPIRED, + models.IncomingTransferableState.REMOVED, + ) + ).exists() + + dbtrimmer_module.BULK_DELETION_SIZE = old_value diff --git a/backend/tests/destination/integration/cleaning/s3remover/__init__.py b/backend/tests/destination/integration/cleaning/s3remover/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/integration/cleaning/s3remover/conftest.py b/backend/tests/destination/integration/cleaning/s3remover/conftest.py new file mode 100644 index 0000000..8adbe65 --- /dev/null +++ b/backend/tests/destination/integration/cleaning/s3remover/conftest.py @@ -0,0 +1,17 @@ +from typing import Iterator + +import pytest +from faker import Faker + +from eurydice.destination.core import models +from tests.destination.integration import factory as destination_factory + + +@pytest.fixture() +def success_incoming_transferable( + faker: Faker, +) -> Iterator[models.IncomingTransferable]: + with destination_factory.s3_stored_incoming_transferable( + data=faker.binary(length=1024), state=models.IncomingTransferableState.SUCCESS + ) as obj: + yield obj diff --git a/backend/tests/destination/integration/cleaning/s3remover/test_main.py b/backend/tests/destination/integration/cleaning/s3remover/test_main.py new file mode 100644 index 0000000..602de44 --- /dev/null +++ b/backend/tests/destination/integration/cleaning/s3remover/test_main.py @@ -0,0 +1,253 @@ +import datetime +from unittest import mock + +import pytest +from django import conf +from django.utils import timezone +from minio.error import S3Error + +from eurydice.common import minio +from eurydice.common.utils import signals +from eurydice.destination.cleaning.s3remover.s3remover import DestinationS3Remover +from eurydice.destination.core import models +from tests.destination.integration import factory + + +@pytest.mark.django_db() +def test_select_transferables_to_remove_success_no_transferable(): + s3remover = DestinationS3Remover() + with pytest.raises(StopIteration): + next(s3remover._select_transferables_to_remove()) + + +@pytest.mark.django_db() +def test_select_transferables_to_remove_success_single_transferable( + settings: conf.Settings, +): + settings.S3REMOVER_EXPIRE_TRANSFERABLES_AFTER = datetime.timedelta(seconds=1) + + finished_at = ( + timezone.now() + - settings.S3REMOVER_EXPIRE_TRANSFERABLES_AFTER + - datetime.timedelta(seconds=1) + ) + transferable = factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.SUCCESS, + created_at=finished_at, + finished_at=finished_at, + ) + + factory.IncomingTransferableFactory(state=models.IncomingTransferableState.ONGOING) + + s3remover = DestinationS3Remover() + assert next(s3remover._select_transferables_to_remove()).id == transferable.id + + +@pytest.mark.django_db() +def test_select_transferables_to_remove_success_multiple_transferables( + settings: conf.Settings, +): + settings.S3REMOVER_EXPIRE_TRANSFERABLES_AFTER = datetime.timedelta(seconds=1) + + nb_success_transferables = 3 + success_transferable_id = set() + + for _ in range(nb_success_transferables): + finished_at = ( + timezone.now() + - settings.S3REMOVER_EXPIRE_TRANSFERABLES_AFTER + - datetime.timedelta(seconds=1) + ) + transferable = factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.SUCCESS, + created_at=finished_at, + finished_at=finished_at, + ) + success_transferable_id.add(transferable.id) + + # ongoing transferable should not be removed + ongoing_transferable = factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.ONGOING + ) + created_at = ( + timezone.now() + - settings.S3REMOVER_EXPIRE_TRANSFERABLES_AFTER + + datetime.timedelta(seconds=1) + ) + factory.S3UploadPartFactory( + part_number=1, incoming_transferable=ongoing_transferable, created_at=created_at + ) + + s3remover = DestinationS3Remover() + retrieved = {t.id for t in s3remover._select_transferables_to_remove()} + assert retrieved == success_transferable_id + + +@pytest.mark.django_db() +def test_select_transferables_to_remove_success_and_ongoing_transferables( + settings: conf.Settings, +): + settings.S3REMOVER_EXPIRE_TRANSFERABLES_AFTER = datetime.timedelta(seconds=1) + + nb_ongoing_transferables_to_keep = 2 + nb_ongoing_transferables_to_remove = 4 + nb_success_transferables_to_keep = 3 + nb_success_transferables_to_remove = 5 + expected_selected_transferable_id = set() + + unexpired_date = ( + timezone.now() + - settings.S3REMOVER_EXPIRE_TRANSFERABLES_AFTER + + datetime.timedelta(seconds=1) + ) + expired_date = ( + timezone.now() + - settings.S3REMOVER_EXPIRE_TRANSFERABLES_AFTER + - datetime.timedelta(seconds=1) + ) + + for _ in range(nb_ongoing_transferables_to_keep): + transferable = factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.ONGOING, + created_at=unexpired_date, + ) + s3part = factory.S3UploadPartFactory( + part_number=1, + incoming_transferable=transferable, + ) + s3part.created_at = unexpired_date + s3part.save() + + for _ in range(nb_ongoing_transferables_to_remove): + transferable = factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.ONGOING, + created_at=expired_date, + ) + s3part = factory.S3UploadPartFactory( + part_number=1, + incoming_transferable=transferable, + ) + s3part.created_at = expired_date + s3part.save() + expected_selected_transferable_id.add(transferable.id) + + for _ in range(nb_success_transferables_to_keep): + factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.SUCCESS, + created_at=unexpired_date, + finished_at=unexpired_date, + ) + + for _ in range(nb_success_transferables_to_remove): + transferable = factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.SUCCESS, + created_at=expired_date, + finished_at=expired_date, + ) + expected_selected_transferable_id.add(transferable.id) + + s3remover = DestinationS3Remover() + retrieved = {t.id for t in s3remover._select_transferables_to_remove()} + assert retrieved == expected_selected_transferable_id + + +@pytest.mark.django_db() +def test_remove_transferable_success( + caplog: pytest.LogCaptureFixture, + success_incoming_transferable: models.IncomingTransferable, +): + s3remover = DestinationS3Remover() + s3remover._remove_transferable(success_incoming_transferable) + + success_incoming_transferable.refresh_from_db() + assert ( + success_incoming_transferable.state == models.IncomingTransferableState.EXPIRED + ) + + with pytest.raises(S3Error) as exc: + minio.client.get_object( + bucket_name=success_incoming_transferable.s3_bucket_name, + object_name=success_incoming_transferable.s3_object_name, + ) + + assert exc.value.code == "NoSuchKey" + assert caplog.messages == [ + f"The IncomingTransferable {success_incoming_transferable.id} has been marked " + f"as {models.IncomingTransferableState.EXPIRED.value}, " + f"and its data removed from the storage." + ] + + +@pytest.mark.django_db() +@mock.patch( + "eurydice.common.minio.client.remove_object", + side_effect=RuntimeError("Oh no!"), +) +def test_remove_transferable_error_ensure_atomicity(mocked_remove_object: mock.Mock): + transferable = factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.SUCCESS + ) + + s3remover = DestinationS3Remover() + with pytest.raises(RuntimeError, match="Oh no!"): + s3remover._remove_transferable(transferable) + + transferable.refresh_from_db() + assert transferable.state == models.IncomingTransferableState.SUCCESS + mocked_remove_object.assert_called_once() + + +@pytest.mark.django_db() +@mock.patch.object(signals.BooleanCondition, "__bool__") +@mock.patch("time.sleep") +def test_loop_success_no_transferable( + time_sleep: mock.Mock, + boolean_cond: mock.Mock, +): + boolean_cond.side_effect = [True, True, False] + s3remover = DestinationS3Remover() + s3remover._loop() + assert time_sleep.call_count == 2 + + +@pytest.mark.django_db() +@mock.patch.object(signals.BooleanCondition, "__bool__") +@mock.patch("time.sleep") +def test_loop_success_remove_transferable( + time_sleep: mock.Mock, + boolean_cond: mock.Mock, + settings: conf.Settings, + success_incoming_transferable: models.IncomingTransferable, +): + settings.S3REMOVER_EXPIRE_TRANSFERABLES_AFTER = datetime.timedelta(seconds=1) + + finished_at = ( + timezone.now() + - settings.S3REMOVER_EXPIRE_TRANSFERABLES_AFTER + - datetime.timedelta(seconds=1) + ) + + success_incoming_transferable.created_at = finished_at + success_incoming_transferable.finished_at = finished_at + success_incoming_transferable.save(update_fields=["created_at", "finished_at"]) + + assert ( + success_incoming_transferable.state == models.IncomingTransferableState.SUCCESS + ) + + boolean_cond.side_effect = [True, True, False] + s3remover = DestinationS3Remover() + s3remover._loop() + + success_incoming_transferable.refresh_from_db() + assert ( + success_incoming_transferable.state == models.IncomingTransferableState.EXPIRED + ) + + with pytest.raises(S3Error) as exc: + minio.client.get_object( + bucket_name=success_incoming_transferable.s3_bucket_name, + object_name=success_incoming_transferable.s3_object_name, + ) + + assert exc.value.code == "NoSuchKey" diff --git a/backend/tests/destination/integration/cleaning/s3remover/test_s3remover.py b/backend/tests/destination/integration/cleaning/s3remover/test_s3remover.py new file mode 100644 index 0000000..7e4085c --- /dev/null +++ b/backend/tests/destination/integration/cleaning/s3remover/test_s3remover.py @@ -0,0 +1,36 @@ +import os +import subprocess +import sys + +import pytest +from django.conf import settings + +from eurydice.destination.cleaning import s3remover + + +@pytest.mark.django_db() +def test_start_and_graceful_shutdown(): + with subprocess.Popen( + [sys.executable, "-m", s3remover.__name__], + cwd=os.path.dirname(settings.BASE_DIR), + stderr=subprocess.PIPE, + env={ + "DB_NAME": settings.DATABASES["default"]["NAME"], + "DB_USER": settings.DATABASES["default"]["USER"], + "DB_PASSWORD": settings.DATABASES["default"]["PASSWORD"], + "DB_HOST": settings.DATABASES["default"]["HOST"], + "DB_PORT": str(settings.DATABASES["default"]["PORT"]), + "MINIO_ENDPOINT": settings.MINIO_ENDPOINT, + "MINIO_ACCESS_KEY": settings.MINIO_ACCESS_KEY, + "MINIO_SECRET_KEY": settings.MINIO_SECRET_KEY, + "MINIO_BUCKET_NAME": settings.MINIO_BUCKET_NAME, + "TRANSFERABLE_STORAGE_DIR": settings.TRANSFERABLE_STORAGE_DIR, + "USER_ASSOCIATION_TOKEN_SECRET_KEY": settings.USER_ASSOCIATION_TOKEN_SECRET_KEY, # noqa: E501 + }, + ) as proc: + while b"Ready" not in proc.stderr.readline(): + pass + + proc.terminate() + return_code = proc.wait() + assert return_code == 0 diff --git a/backend/tests/destination/integration/core/__init__.py b/backend/tests/destination/integration/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/integration/core/management/__init__.py b/backend/tests/destination/integration/core/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/integration/core/management/commands/__init__.py b/backend/tests/destination/integration/core/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/integration/core/management/commands/test_populate_db.py b/backend/tests/destination/integration/core/management/commands/test_populate_db.py new file mode 100644 index 0000000..c864c98 --- /dev/null +++ b/backend/tests/destination/integration/core/management/commands/test_populate_db.py @@ -0,0 +1,17 @@ +import pytest +from django.contrib import auth +from django.core.management import call_command + +from eurydice.destination.core import models + + +@pytest.mark.django_db() +def test_populate_db(): + args = [] + opts = {"users": 1, "incoming_transferables": 2, "s3_uploaded_parts": 3} + call_command("populate_db", *args, **opts) + + assert models.UserProfile.objects.count() == opts["users"] + assert auth.get_user_model().objects.count() == opts["users"] + assert models.IncomingTransferable.objects.count() == opts["incoming_transferables"] + assert models.S3UploadPart.objects.count() == opts["s3_uploaded_parts"] diff --git a/backend/tests/destination/integration/core/models/__init__.py b/backend/tests/destination/integration/core/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/integration/core/models/test_incoming_transferable.py b/backend/tests/destination/integration/core/models/test_incoming_transferable.py new file mode 100644 index 0000000..5995e99 --- /dev/null +++ b/backend/tests/destination/integration/core/models/test_incoming_transferable.py @@ -0,0 +1,161 @@ +import datetime +from typing import Callable +from typing import Optional + +import factory as factory_boy +import freezegun +import pytest +from django import conf +from django.utils import timezone + +from eurydice.destination.core import models +from tests.destination.integration import factory + + +@pytest.mark.parametrize("has_s3_upload_parts", [True, False]) +@pytest.mark.parametrize("save", [True, False]) +@pytest.mark.parametrize( + ("update_state_func", "target_state"), + [ + ( + models.IncomingTransferable.mark_as_expired, + models.IncomingTransferableState.EXPIRED, + ), + ( + models.IncomingTransferable.mark_as_removed, + models.IncomingTransferableState.REMOVED, + ), + ], +) +@pytest.mark.django_db() +def test_mark_as( + update_state_func: Callable[[models.IncomingTransferable, bool], None], + target_state: models.IncomingTransferableState, + save: bool, + has_s3_upload_parts: bool, +): + transferable = factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.SUCCESS + ) + if has_s3_upload_parts: + factory.S3UploadPartFactory.create_batch( + 3, + part_number=factory_boy.Iterator([1, 2, 3]), + incoming_transferable=transferable, + ) + + update_state_func(transferable, save=save) + + if save: + transferable.refresh_from_db() + + assert transferable.state == target_state + + assert ( + not models.S3UploadPart.objects.filter(incoming_transferable=transferable) + .only("id") + .exists() + ) + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + ( + "size", + "bytes_received", + "expected_progress", + ), + [ + (None, 0, None), + (None, 1024, None), + (1024, 1024, 100), + (1024, 0, 0), + (1024, 512, 50), + (0, 0, 100), + ], +) +def test_incoming_transferable_progress( + size: Optional[int], + bytes_received: int, + expected_progress: int, +): + if bytes_received == size: + state = models.IncomingTransferableState.SUCCESS + else: + state = models.IncomingTransferableState.ONGOING + + incoming_transferable = factory.IncomingTransferableFactory( + size=size, bytes_received=bytes_received, state=state + ) + + queried_incoming_transferable = models.IncomingTransferable.objects.get( + id=incoming_transferable.id + ) + + assert queried_incoming_transferable.progress == expected_progress + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + ("finished_at", "state", "s3remover_retention", "expected_expires_at"), + [ + ( + datetime.datetime( + year=1983, month=6, day=21, tzinfo=timezone.get_current_timezone() + ), + models.IncomingTransferableState.SUCCESS, + datetime.timedelta(days=1), + datetime.datetime( + year=1983, month=6, day=22, tzinfo=timezone.get_current_timezone() + ), + ), + ( + None, + models.IncomingTransferableState.ONGOING, + datetime.timedelta(days=1), + None, + ), + ( + timezone.now(), + models.IncomingTransferableState.ERROR, + datetime.timedelta(days=1), + None, + ), + ( + timezone.now(), + models.IncomingTransferableState.REVOKED, + datetime.timedelta(days=1), + None, + ), + ( + timezone.now(), + models.IncomingTransferableState.EXPIRED, + datetime.timedelta(days=1), + None, + ), + ], +) +def test_incoming_transferable_expires_at( + finished_at: Optional[datetime.datetime], + state: models.IncomingTransferableState, + s3remover_retention: datetime.timedelta, + expected_expires_at: Optional[datetime.datetime], + settings: conf.Settings, +): + settings.S3REMOVER_EXPIRE_TRANSFERABLES_AFTER = s3remover_retention + + with freezegun.freeze_time( + datetime.datetime( + year=1982, month=6, day=21, tzinfo=timezone.get_current_timezone() + ) + ): + incoming_transferable = factory.IncomingTransferableFactory( + state=state, + finished_at=finished_at, + ) + + queried_incoming_transferable = models.IncomingTransferable.objects.get( + id=incoming_transferable.id + ) + + assert queried_incoming_transferable.expires_at == expected_expires_at diff --git a/backend/tests/destination/integration/endpoints/__init__.py b/backend/tests/destination/integration/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/integration/endpoints/test_api_schema.py b/backend/tests/destination/integration/endpoints/test_api_schema.py new file mode 100644 index 0000000..0aa203e --- /dev/null +++ b/backend/tests/destination/integration/endpoints/test_api_schema.py @@ -0,0 +1,14 @@ +import django.urls +import pytest +from rest_framework import status +from rest_framework import test + + +@pytest.mark.django_db() +def test_post_association_token_success(api_client: test.APIClient): + url = django.urls.reverse("api-schema") + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + assert response.headers["Content-Type"] == "application/json" + assert response.data["info"]["title"] == "Eurydice destination API" diff --git a/backend/tests/destination/integration/endpoints/test_metrics.py b/backend/tests/destination/integration/endpoints/test_metrics.py new file mode 100644 index 0000000..4e41ae0 --- /dev/null +++ b/backend/tests/destination/integration/endpoints/test_metrics.py @@ -0,0 +1,172 @@ +from datetime import datetime +from datetime import timedelta +from typing import List + +import dateutil +import freezegun +import pytest +from django import conf +from django.contrib.auth.models import Permission +from django.urls import reverse +from django.utils import timezone +from faker import Faker +from rest_framework import test + +from eurydice.destination.api.views import metrics +from eurydice.destination.core.models import IncomingTransferable +from eurydice.destination.core.models import IncomingTransferableState as States +from eurydice.destination.core.models import LastPacketReceivedAt +from tests.destination.integration import factory + + +def create_transferable(state: States, date: datetime) -> IncomingTransferable: + with freezegun.freeze_time(date): + return factory.IncomingTransferableFactory( + state=state, + _finished_at=date, + ) + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + ( + "old_states_repetitions", + "recent_states_repetitions", + ), + [ + ([2, 4, 6, 8, 10, 12], [1, 3, 5, 7, 9, 11]), + ([0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0]), + ([0, 2, 0, 2, 0, 2], [0, 2, 0, 2, 0, 2]), + ([2, 0, 2, 0, 2, 0], [2, 0, 2, 0, 2, 0]), + ([1, 0, 4, 2, 0, 1], [0, 4, 2, 0, 1, 1]), + ], +) +def test__generate_metrics( + faker: Faker, + settings: conf.Settings, + api_client: test.APIClient, + old_states_repetitions: List[int], + recent_states_repetitions: List[int], +): + # Test preparation + + # We make sure the parameterization is consistent with the amount of + # known states. We use this data structure in parameterization because it + # is more concise (and readable) than a Dict[States, int] + assert len(old_states_repetitions) == len(States.values) + assert len(recent_states_repetitions) == len(States.values) + + old_transferable_nb = { + States.ONGOING: old_states_repetitions[0], + States.SUCCESS: old_states_repetitions[1], + States.ERROR: old_states_repetitions[2], + States.REVOKED: old_states_repetitions[3], + States.EXPIRED: old_states_repetitions[4], + States.REMOVED: old_states_repetitions[5], + } + + recent_transferable_nb = { + States.ONGOING: recent_states_repetitions[0], + States.SUCCESS: recent_states_repetitions[1], + States.ERROR: recent_states_repetitions[2], + States.REVOKED: recent_states_repetitions[3], + States.EXPIRED: recent_states_repetitions[4], + States.REMOVED: recent_states_repetitions[5], + } + + settings.METRICS_SLIDING_WINDOW = 3600 + + # With the current fixed faker seed, faker just happens to return a datetime + # within 4000 seconds of a "spring forward", due to daylight saving time, + # which means that while the following code passes the test, it would fail + # if recent_date was using the fake datetime + # and old_date was computed using recent_date - 4000 seconds. + old_date = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + recent_date = old_date + timedelta(seconds=4000) + + for state, repetitions in old_transferable_nb.items(): + for _ in range(repetitions): + create_transferable(state=state, date=old_date) + + for state, repetitions in recent_transferable_nb.items(): + for _ in range(repetitions): + create_transferable(state=state, date=recent_date) + + expected = { + "ongoing_transferables": ( + old_transferable_nb[States.ONGOING] + recent_transferable_nb[States.ONGOING] + ), + "recent_successes": ( + recent_transferable_nb[States.SUCCESS] + + recent_transferable_nb[States.EXPIRED] + + recent_transferable_nb[States.REMOVED] + ), + "recent_errors": recent_transferable_nb[States.ERROR], + "last_packet_received_at": None, + } + + # Actual test + user_profile = factory.UserProfileFactory() + permission = Permission.objects.get(codename="view_rolling_metrics") + user_profile.user.user_permissions.add(permission) + + with freezegun.freeze_time(recent_date): + assert metrics.MetricsView.get_object(None) == expected + + api_client.force_authenticate(user_profile.user) + url = reverse("metrics") + response = api_client.get(url) + assert response.json() == expected + + +@pytest.mark.django_db() +def test__last_packet_received_at( + faker: Faker, + api_client: test.APIClient, +): + # Test preparation + LastPacketReceivedAt.update() + + timestamp = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + with freezegun.freeze_time(timestamp): + LastPacketReceivedAt.update() + + user_profile = factory.UserProfileFactory() + permission = Permission.objects.get(codename="view_rolling_metrics") + user_profile.user.user_permissions.add(permission) + api_client.force_authenticate(user_profile.user) + + # Actual test + url = reverse("metrics") + response = api_client.get(url) + response_json = response.json() + assert dateutil.parser.parse(response_json["last_packet_received_at"]) == timestamp + + +@pytest.mark.django_db() +def test__metrics_permissions_deny(api_client: test.APIClient): + user_profile = factory.UserProfileFactory() + url = reverse("metrics") + + response = api_client.get(url) + + assert response.status_code == 401 + + api_client.force_authenticate(user_profile.user) + response = api_client.get(url) + + assert response.status_code == 403 + assert "You do not have permission" in response.json()["detail"] + + +@pytest.mark.django_db() +def test__metrics_permissions_allow(api_client: test.APIClient): + user_profile = factory.UserProfileFactory() + permission = Permission.objects.get(codename="view_rolling_metrics") + user_profile.user.user_permissions.add(permission) + + api_client.force_authenticate(user_profile.user) + url = reverse("metrics") + response = api_client.get(url) + + assert response.status_code == 200 diff --git a/backend/tests/destination/integration/endpoints/test_pagination.py b/backend/tests/destination/integration/endpoints/test_pagination.py new file mode 100644 index 0000000..6885e7b --- /dev/null +++ b/backend/tests/destination/integration/endpoints/test_pagination.py @@ -0,0 +1,24 @@ +from eurydice.destination.core.models import IncomingTransferable +from eurydice.destination.core.models import IncomingTransferableState +from tests.common.integration.endpoints import pagination +from tests.destination.integration import factory + + +def make_transferables(count: int, **kwargs): + factory.IncomingTransferableFactory.create_batch(count, **kwargs) + + +class TestPagination(pagination.PaginationTestsSuperclass): + user_profile_factory = factory.UserProfileFactory + transferable_class = IncomingTransferable + success_state = IncomingTransferableState.SUCCESS + error_state = IncomingTransferableState.ERROR + make_transferables = make_transferables + + +class TestPaginationInTransaction(pagination.PaginationTestsInTransactionSuperclass): + user_profile_factory = factory.UserProfileFactory + transferable_class = IncomingTransferable + success_state = IncomingTransferableState.SUCCESS + error_state = IncomingTransferableState.ERROR + make_transferables = make_transferables diff --git a/backend/tests/destination/integration/endpoints/test_status.py b/backend/tests/destination/integration/endpoints/test_status.py new file mode 100644 index 0000000..0fb54d2 --- /dev/null +++ b/backend/tests/destination/integration/endpoints/test_status.py @@ -0,0 +1,36 @@ +import dateutil +import freezegun +import pytest +from django.urls import reverse +from django.utils import timezone +from faker import Faker +from rest_framework import test + +from eurydice.destination.api.views import StatusView +from eurydice.destination.core.models import LastPacketReceivedAt +from tests.destination.integration import factory + + +@pytest.mark.django_db() +def test_get_status( + faker: Faker, + api_client: test.APIClient, +): + # Test preparation + LastPacketReceivedAt.update() + + timestamp = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + with freezegun.freeze_time(timestamp): + LastPacketReceivedAt.update() + + user_profile = factory.UserProfileFactory() + api_client.force_authenticate(user_profile.user) + + # Actual test + assert StatusView.get_object(None) == {"last_packet_received_at": timestamp} + + url = reverse("status") + response = api_client.get(url) + response_json = response.json() + assert len(response_json) == 1 + assert dateutil.parser.parse(response_json["last_packet_received_at"]) == timestamp diff --git a/backend/tests/destination/integration/endpoints/test_transferables.py b/backend/tests/destination/integration/endpoints/test_transferables.py new file mode 100644 index 0000000..6e24072 --- /dev/null +++ b/backend/tests/destination/integration/endpoints/test_transferables.py @@ -0,0 +1,438 @@ +import base64 +import contextlib +import io +from typing import Dict + +import dateutil.parser +import pytest +from django.urls import reverse +from faker import Faker +from minio.error import S3Error +from rest_framework import status +from rest_framework import test + +from eurydice.common import minio +from eurydice.destination.core import models +from tests.destination.integration import factory + + +@contextlib.contextmanager +def s3_stored_incoming_transferable(): + obj = factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.SUCCESS + ) + data = b"Lorem ipsum dolor sit amet" + + try: + minio.client.make_bucket(bucket_name=obj.s3_bucket_name) + minio.client.put_object( + bucket_name=obj.s3_bucket_name, + object_name=obj.s3_object_name, + data=io.BytesIO(data), + length=len(data), + ) + + yield obj + finally: + minio.client.remove_object( + bucket_name=obj.s3_bucket_name, object_name=obj.s3_object_name + ) + minio.client.remove_bucket(bucket_name=obj.s3_bucket_name) + + +@pytest.mark.django_db() +class TestIncomingTransferable: + def test_list_incoming_transferables(self, api_client: test.APIClient) -> None: + obj = factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.ONGOING + ) + + api_client.force_login(user=obj.user_profile.user) + url = reverse("transferable-list") + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 1 + assert len(response.data["results"]) == 1 + + data = response.data["results"][0] + assert data["name"] == obj.name + assert bytes.fromhex(data["sha1"]) == obj.sha1 + assert data["size"] == obj.size + assert data["user_provided_meta"] == obj.user_provided_meta + assert data["state"] == obj.state.value + assert data["progress"] == 0 + assert data["expires_at"] is None + assert data["bytes_received"] == obj.bytes_received + + if data["finished_at"] is None: + assert obj.finished_at is None + else: + assert dateutil.parser.parse(data["finished_at"]) == obj.finished_at + + def test_list_incoming_transferables_error_not_authenticated( + self, api_client: test.APIClient + ): + url = reverse("transferable-list") + response = api_client.get(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response.data["detail"].code == "not_authenticated" + + def test_retrieve_incoming_transferable(self, api_client: test.APIClient) -> None: + obj = factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.ONGOING + ) + + api_client.force_login(user=obj.user_profile.user) + url = reverse("transferable-detail", kwargs={"pk": obj.id}) + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + + data = response.data + assert data["name"] == obj.name + assert bytes.fromhex(data["sha1"]) == obj.sha1 + assert data["size"] == obj.size + assert data["user_provided_meta"] == obj.user_provided_meta + assert data["state"] == obj.state.value + assert data["progress"] == 0 + assert data["expires_at"] is None + assert data["bytes_received"] == obj.bytes_received + + if data["finished_at"] is None: + assert obj.finished_at is None + else: + assert dateutil.parser.parse(data["finished_at"]) == obj.finished_at + + def test_retrieve_incoming_transferable_error_not_authenticated( + self, api_client: test.APIClient + ): + obj = factory.IncomingTransferableFactory() + url = reverse("transferable-detail", kwargs={"pk": obj.id}) + response = api_client.get(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response.data["detail"].code == "not_authenticated" + + def test_retrieve_outgoing_transferable_error_not_owner( + self, api_client: test.APIClient + ): + obj = factory.IncomingTransferableFactory() + another_user = factory.UserProfileFactory().user + + api_client.force_login(user=another_user) + url = reverse("transferable-detail", kwargs={"pk": obj.id}) + response = api_client.get(url) + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.data["detail"].code == "not_found" + + def test_retrieve_incoming_transferable_error_not_associated( + self, api_client: test.APIClient + ): + obj = factory.IncomingTransferableFactory() + another_user = factory.UserFactory() + + api_client.force_login(user=another_user) + url = reverse("transferable-detail", kwargs={"pk": obj.id}) + response = api_client.get(url) + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.data["detail"].code == "permission_denied" + + def test_retrieve_incoming_transferable_error_no_transferable( + self, + api_client: test.APIClient, + faker: Faker, + ): + user = factory.UserProfileFactory().user + + api_client.force_login(user=user) + url = reverse("transferable-detail", kwargs={"pk": faker.uuid4()}) + response = api_client.get(url) + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.data["detail"].code == "not_found" + + def test_destroy_incoming_transferable_error_not_authenticated( + self, api_client: test.APIClient + ) -> None: + obj = factory.IncomingTransferableFactory() + url = reverse("transferable-detail", kwargs={"pk": obj.id}) + response = api_client.delete(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response.data["detail"].code == "not_authenticated" + + def test_destroy_incoming_transferable_error_not_owner( + self, api_client: test.APIClient + ) -> None: + obj = factory.IncomingTransferableFactory() + another_user = factory.UserProfileFactory().user + + api_client.force_login(user=another_user) + url = reverse("transferable-detail", kwargs={"pk": obj.id}) + response = api_client.delete(url) + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.data["detail"].code == "not_found" + + def test_destroy_incoming_transferable_error_not_associated( + self, api_client: test.APIClient + ) -> None: + obj = factory.IncomingTransferableFactory() + another_user = factory.UserFactory() + + api_client.force_login(user=another_user) + url = reverse("transferable-detail", kwargs={"pk": obj.id}) + response = api_client.delete(url) + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.data["detail"].code == "permission_denied" + + def test_destroy_incoming_transferable_error_no_transferable( + self, + api_client: test.APIClient, + faker: Faker, + ): + user = factory.UserProfileFactory().user + + api_client.force_login(user=user) + url = reverse("transferable-detail", kwargs={"pk": faker.uuid4()}) + response = api_client.delete(url) + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.data["detail"].code == "not_found" + + @pytest.mark.parametrize("is_multipart", [True, False]) + def test_destroy_incoming_transferable_success( + self, is_multipart: bool, api_client: test.APIClient + ) -> None: + with s3_stored_incoming_transferable() as obj: + if is_multipart: + for i in range(1, 3): + factory.S3UploadPartFactory( + part_number=i, incoming_transferable=obj + ) + + api_client.force_login(user=obj.user_profile.user) + url = reverse("transferable-detail", kwargs={"pk": obj.id}) + response = api_client.delete(url) + + assert response.status_code == status.HTTP_204_NO_CONTENT + + with pytest.raises(S3Error) as error: + minio.client.get_object( + bucket_name=obj.s3_bucket_name, object_name=obj.s3_object_name + ) + + assert error.value.code == "NoSuchKey" + + obj.refresh_from_db() + assert obj.state == models.IncomingTransferableState.REMOVED + + @pytest.mark.parametrize( + "incoming_transferable_state", + [ + models.IncomingTransferableState.ONGOING, + models.IncomingTransferableState.EXPIRED, + models.IncomingTransferableState.ERROR, + models.IncomingTransferableState.REMOVED, + ], + ) + def test_destroy_incoming_transferable_ongoing_error( + self, + incoming_transferable_state: models.IncomingTransferableState, + api_client: test.APIClient, + ) -> None: + obj = factory.IncomingTransferableFactory(state=incoming_transferable_state) + api_client.force_login(user=obj.user_profile.user) + url = reverse("transferable-detail", kwargs={"pk": obj.id}) + response = api_client.delete(url) + + assert response.status_code == status.HTTP_409_CONFLICT + assert response.data["detail"].code == "UnsuccessfulTransferableError" + + def test_download_incoming_transferable_error_not_authenticated( + self, api_client: test.APIClient + ) -> None: + obj = factory.IncomingTransferableFactory() + url = reverse("transferable-download", kwargs={"pk": obj.id}) + response = api_client.get(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response.headers["Content-Type"] == "application/json" + assert response.data["detail"].code == "not_authenticated" + + def test_download_incoming_transferable_error_not_owner( + self, api_client: test.APIClient + ) -> None: + obj = factory.IncomingTransferableFactory() + another_user = factory.UserProfileFactory().user + + api_client.force_login(user=another_user) + url = reverse("transferable-download", kwargs={"pk": obj.id}) + response = api_client.get(url) + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.headers["Content-Type"] == "application/json" + assert response.data["detail"].code == "not_found" + + def test_download_incoming_transferable_error_not_associated( + self, api_client: test.APIClient + ) -> None: + obj = factory.IncomingTransferableFactory() + another_user = factory.UserFactory() + + api_client.force_login(user=another_user) + url = reverse("transferable-download", kwargs={"pk": obj.id}) + response = api_client.get(url) + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.headers["Content-Type"] == "application/json" + assert response.data["detail"].code == "permission_denied" + + def test_download_incoming_transferable_error_no_transferable( + self, + api_client: test.APIClient, + faker: Faker, + ): + user = factory.UserProfileFactory().user + + api_client.force_login(user=user) + url = reverse("transferable-download", kwargs={"pk": faker.uuid4()}) + response = api_client.get(url) + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.headers["Content-Type"] == "application/json" + assert response.data["detail"].code == "not_found" + + @pytest.mark.parametrize( + ( + "incoming_transferable_state", + "expected_http_status_code", + "expected_error_detail_code", + ), + [ + ( + models.IncomingTransferableState.ERROR, + status.HTTP_403_FORBIDDEN, + "TransferableErroredError", + ), + ( + models.IncomingTransferableState.ONGOING, + status.HTTP_409_CONFLICT, + "TransferableOngoingError", + ), + ( + models.IncomingTransferableState.EXPIRED, + status.HTTP_410_GONE, + "TransferableExpiredError", + ), + ( + models.IncomingTransferableState.REVOKED, + status.HTTP_410_GONE, + "TransferableRevokedError", + ), + ( + models.IncomingTransferableState.REMOVED, + status.HTTP_410_GONE, + "TransferableRemovedError", + ), + ], + ) + def test_download_incoming_transferable_error_due_to_transferable_state( + self, + incoming_transferable_state: models.IncomingTransferableState, + expected_http_status_code: int, + expected_error_detail_code: str, + api_client: test.APIClient, + ) -> None: + obj = factory.IncomingTransferableFactory(state=incoming_transferable_state) + + api_client.force_login(user=obj.user_profile.user) + + url = reverse("transferable-download", kwargs={"pk": obj.id}) + response = api_client.get(url) + + assert response.status_code == expected_http_status_code + assert response.data["detail"].code == expected_error_detail_code + + def test_download_incoming_transferable_s3_object_not_found( + self, + api_client: test.APIClient, + ) -> None: + with factory.s3_stored_incoming_transferable( + data=b"", + state=models.IncomingTransferableState.SUCCESS, + ) as transferable: + minio.client.remove_object( + bucket_name=transferable.s3_bucket_name, + object_name=transferable.s3_object_name, + ) + + api_client.force_login(user=transferable.user_profile.user) + url = reverse("transferable-download", kwargs={"pk": transferable.id}) + + response = api_client.get(url) + + assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + + transferable.refresh_from_db() + assert transferable.state == models.IncomingTransferableState.ERROR + + @pytest.mark.parametrize( + "http_headers", + [ + {}, + {"HTTP_ACCEPT": "application/octet-stream"}, + {"HTTP_ACCEPT": "application/*"}, + # ignoring the Accept header is valid in this case, see: + # https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 + {"HTTP_ACCEPT": "application/json"}, + ], + ) + @pytest.mark.parametrize( + ("data", "filename"), + [ + (b"", "file.ext"), + (b"Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "file.ext"), + (b"hello", ""), + ], + ) + def test_download_incoming_transferable_success( + self, + data: bytes, + filename: str, + http_headers: Dict[str, str], + api_client: test.APIClient, + faker: Faker, + ) -> None: + data = faker.binary(1024) + + with factory.s3_stored_incoming_transferable( + data=data, + size=len(data), + name=filename, + user_provided_meta={"Metadata-Foo": "Bar", "Metadata-Baz": "Xyz"}, + state=models.IncomingTransferableState.SUCCESS, + ) as obj: + api_client.force_login(user=obj.user_profile.user) + url = reverse("transferable-download", kwargs={"pk": obj.id}) + response = api_client.get(url, **http_headers) + + if not filename: + filename = str(obj.id) + + assert response.status_code == status.HTTP_200_OK + assert ( + response.headers.items() + >= { + "Content-Length": f"{len(data)}", + "Content-Type": "application/octet-stream", + "Content-Disposition": f'attachment; filename="{filename}"', + "Digest": "SHA=" + base64.b64encode(obj.sha1).decode("utf-8"), + }.items() + ) + assert response.getvalue() == data diff --git a/backend/tests/destination/integration/endpoints/test_transferables_filtering.py b/backend/tests/destination/integration/endpoints/test_transferables_filtering.py new file mode 100644 index 0000000..db3cd68 --- /dev/null +++ b/backend/tests/destination/integration/endpoints/test_transferables_filtering.py @@ -0,0 +1,219 @@ +import hashlib +from datetime import datetime +from datetime import timedelta + +import freezegun +import pytest +from django.urls import reverse +from django.utils import timezone +from django.utils.dateparse import parse_datetime +from faker import Faker +from rest_framework import status +from rest_framework import test + +from eurydice.destination.core import models +from tests.destination.integration import factory + + +def _fdate(date: datetime) -> str: + return date.isoformat()[:-6] + "Z" + + +@pytest.mark.django_db() +class TestIncomingTransferableFiltering: + def test_filter_creation_date( + self, + api_client: test.APIClient, + faker: Faker, + ): + user_profile = factory.UserProfileFactory() + + a_date = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + ref_date = a_date + timedelta(days=2) + + for days in (0, 1, 3, 4): + with freezegun.freeze_time(a_date + timedelta(days=days)): + factory.IncomingTransferableFactory(user_profile=user_profile) + + api_client.force_login(user_profile.user) + url = reverse("transferable-list") + response = api_client.get(url, {"created_after": _fdate(ref_date)}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 2 + assert len(response.data["results"]) == 2 + + for data in response.data["results"]: + assert parse_datetime(data["created_at"]) > ref_date + + response = api_client.get( + url, + { + "created_after": _fdate(ref_date), + "page": response.data["pages"]["current"], + }, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 2 + assert len(response.data["results"]) == 2 + + def test_filter_state( + self, + api_client: test.APIClient, + ): + user_profile = factory.UserProfileFactory() + api_client.force_login(user_profile.user) + + for state in models.IncomingTransferableState.names: + factory.IncomingTransferableFactory(state=state, user_profile=user_profile) + + for state in ("ONGOING", "REVOKED", "EXPIRED"): + url = reverse("transferable-list") + response = api_client.get(url, {"state": state}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 1 + assert len(response.data["results"]) == 1 + + for data in response.data["results"]: + assert data["state"] == state + + def test_filter_name( + self, + api_client: test.APIClient, + ): + user_profile = factory.UserProfileFactory() + api_client.force_login(user_profile.user) + + for name in ("aaa.txt", "bbb.txt", "aaa.bin", "txt.aaa", "ccc.txt"): + factory.IncomingTransferableFactory(user_profile=user_profile, name=name) + + url = reverse("transferable-list") + response = api_client.get(url, {"name": ".txt"}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 3 + assert len(response.data["results"]) == 3 + + for data in response.data["results"]: + assert data["name"].endswith(".txt") + + def test_filter_sha1( + self, + api_client: test.APIClient, + ): + user_profile = factory.UserProfileFactory() + api_client.force_login(user_profile.user) + + for name in ("aaa.txt", "bbb.txt", "aaa.bin", "txt.aaa", "ccc.txt"): + factory.IncomingTransferableFactory( + user_profile=user_profile, + name=name, + sha1=hashlib.sha1(name.encode("utf-8")).digest(), + ) + + url = reverse("transferable-list") + response = api_client.get(url, {"sha1": hashlib.sha1(b"txt.aaa").hexdigest()}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 1 + assert len(response.data["results"]) == 1 + + for data in response.data["results"]: + assert data["name"] == "txt.aaa" + + def test_filter_finished_date( + self, + api_client: test.APIClient, + faker: Faker, + ): + user_profile = factory.UserProfileFactory() + + a_date = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + + for days in (-3, -1, 2, 1): + with freezegun.freeze_time(a_date + timedelta(days=-6)): + factory.IncomingTransferableFactory( + user_profile=user_profile, + _finished_at=a_date + timedelta(days=days), + state=models.IncomingTransferableState.SUCCESS, + ) + + api_client.force_login(user_profile.user) + url = reverse("transferable-list") + response = api_client.get(url, {"finished_before": _fdate(a_date)}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 2 + assert len(response.data["results"]) == 2 + + for data in response.data["results"]: + assert parse_datetime(data["finished_at"]) < a_date + + def test_page_size(self, api_client: test.APIClient): + user_profile = factory.UserProfileFactory() + factory.IncomingTransferableFactory.create_batch(55, user_profile=user_profile) + + api_client.force_login(user_profile.user) + url = reverse("transferable-list") + + response = api_client.get(url, {"page_size": 25}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 55 + assert len(response.data["results"]) == 25 + + response = api_client.get(url, {"page_size": 20}) + response = api_client.get( + url, + {"page_size": 20, "delta": 2, "from": response.data["pages"]["current"]}, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 55 + assert len(response.data["results"]) == 15 + + def test_isodate( + self, + api_client: test.APIClient, + faker: Faker, + ): + user_profile = factory.UserProfileFactory() + + a_date = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + + with freezegun.freeze_time(a_date): + factory.IncomingTransferableFactory(user_profile=user_profile) + + # check if the returned date is the exact creation date (same timezone) + api_client.force_login(user_profile.user) + url = reverse("transferable-list") + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 1 + assert len(response.data["results"]) == 1 + + retrieved_str_date = response.data["results"][0]["created_at"] + retrieved_date = parse_datetime(retrieved_str_date) + + assert a_date == retrieved_date + + # check if comparison with the same format than the input is OK : after + api_client.force_login(user_profile.user) + url = reverse("transferable-list") + response = api_client.get(url, {"created_after": retrieved_str_date}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 1 + assert len(response.data["results"]) == 1 + + # check if comparison with the same format than the input is OK : before + api_client.force_login(user_profile.user) + url = reverse("transferable-list") + response = api_client.get(url, {"created_before": retrieved_str_date}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 1 + assert len(response.data["results"]) == 1 diff --git a/backend/tests/destination/integration/endpoints/test_user_association.py b/backend/tests/destination/integration/endpoints/test_user_association.py new file mode 100644 index 0000000..488d64c --- /dev/null +++ b/backend/tests/destination/integration/endpoints/test_user_association.py @@ -0,0 +1,189 @@ +from unittest import mock + +import django.urls +import humanfriendly as hf +import pytest +from django import conf +from django.contrib import auth +from django.utils import timezone +from faker import Faker +from rest_framework import status +from rest_framework import test + +from eurydice.common.api import serializers +from eurydice.destination.core import models +from tests.common.integration import factory as common_factory +from tests.destination.integration import factory + + +@pytest.mark.django_db() +class TestUser: + def test_post_association_token_success_no_user_profile( + self, settings: conf.Settings, api_client: test.APIClient + ): + settings.USER_ASSOCIATION_TOKEN_EXPIRES_AFTER = hf.parse_timespan("1h") + token = common_factory.AssociationTokenFactory() + + payload = serializers.AssociationTokenSerializer(token).data + + user = factory.UserFactory() + assert auth.get_user_model().objects.get() == user + assert ( + auth.get_user_model() + .objects.filter(id=user.id, user_profile__isnull=True) + .exists() + ) + assert not models.UserProfile.objects.exists() + + api_client.force_login(user=user) + url = django.urls.reverse("user-association") + response = api_client.post(url, data=payload, format="json") + + assert response.status_code == status.HTTP_204_NO_CONTENT + assert auth.get_user_model().objects.get() == user + assert models.UserProfile.objects.get( + user=user.id, associated_user_profile_id=token.user_profile_id + ) + + def test_post_association_token_success_existing_user_profile( + self, settings: conf.Settings, api_client: test.APIClient + ): + user = factory.UserFactory() + settings.USER_ASSOCIATION_TOKEN_EXPIRES_AFTER = hf.parse_timespan("1h") + token = common_factory.AssociationTokenFactory() + + factory.UserProfileFactory( + user=None, associated_user_profile_id=token.user_profile_id + ) + + payload = serializers.AssociationTokenSerializer(token).data + api_client.force_login(user=user) + url = django.urls.reverse("user-association") + response = api_client.post(url, data=payload, format="json") + + assert response.status_code == status.HTTP_204_NO_CONTENT + assert models.UserProfile.objects.get( + user=user.id, associated_user_profile_id=token.user_profile_id + ) + + def test_post_association_token_error_not_authenticated( + self, api_client: test.APIClient + ): + url = django.urls.reverse("user-association") + response = api_client.post(url, data={}, format="json") + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response.data["detail"].code == "not_authenticated" + + def test_post_association_token_error_non_readable_token( + self, api_client: test.APIClient + ): + user = factory.UserFactory() + assert not models.UserProfile.objects.exists() + + api_client.force_login(user=user) + url = django.urls.reverse("user-association") + response = api_client.post(url, data={"token": "foo bar baz"}, format="json") + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data["token"].code == "invalid" + assert str(response.data["token"]) == "Malformed token." + assert not models.UserProfile.objects.exists() + + def test_post_association_token_error_invalid_token_signature( + self, settings: conf.Settings, api_client: test.APIClient + ): + settings.USER_ASSOCIATION_TOKEN_SECRET_KEY = ( + "kzuzabvqkcc8b4frle16pptynbrlyo6pmfvx" + ) + settings.USER_ASSOCIATION_TOKEN_EXPIRES_AFTER = hf.parse_timespan("1h") + + forged_token = common_factory.AssociationTokenFactory() + payload = serializers.AssociationTokenSerializer(forged_token).data + + user = factory.UserFactory() + assert not models.UserProfile.objects.exists() + + settings.USER_ASSOCIATION_TOKEN_SECRET_KEY = "0" * 36 + + api_client.force_login(user=user) + url = django.urls.reverse("user-association") + response = api_client.post(url, data=payload, format="json") + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data["token"].code == "invalid" + assert str(response.data["token"]) == "Invalid association token signature." + assert not models.UserProfile.objects.exists() + + def test_post_association_token_error_expired_token( + self, faker: Faker, api_client: test.APIClient + ): + token = common_factory.AssociationTokenFactory( + expires_at=faker.date_time_this_decade( + tzinfo=timezone.get_current_timezone() + ) + ) + token.verify_validity_time = mock.Mock() + payload = serializers.AssociationTokenSerializer(token).data + + user = factory.UserFactory() + assert not models.UserProfile.objects.exists() + + api_client.force_login(user=user) + url = django.urls.reverse("user-association") + response = api_client.post(url, data=payload, format="json") + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data["token"].code == "invalid" + assert str(response.data["token"]) == "The association token has expired." + assert not models.UserProfile.objects.exists() + + def test_post_association_token_error_already_associated( + self, settings: conf.Settings, api_client: test.APIClient + ): + user_profile = factory.UserProfileFactory() + + settings.USER_ASSOCIATION_TOKEN_EXPIRES_AFTER = hf.parse_timespan("1h") + token = common_factory.AssociationTokenFactory() + + payload = serializers.AssociationTokenSerializer(token).data + + assert models.UserProfile.objects.get() == user_profile + + api_client.force_login(user=user_profile.user) + url = django.urls.reverse("user-association") + response = api_client.post(url, data=payload, format="json") + + assert response.status_code == status.HTTP_409_CONFLICT + assert response.data["detail"].code == "AlreadyAssociatedError" + assert ( + str(response.data["detail"]) + == "The user is already associated with a user from the origin side." + ) + assert models.UserProfile.objects.get() == user_profile + + def test_post_association_token_error_profile_already_associated( + self, settings: conf.Settings, api_client: test.APIClient + ): + settings.USER_ASSOCIATION_TOKEN_EXPIRES_AFTER = hf.parse_timespan("1h") + token = common_factory.AssociationTokenFactory() + + factory.UserProfileFactory( + user=None, associated_user_profile_id=token.user_profile_id + ) + + payload = serializers.AssociationTokenSerializer(token).data + + url = django.urls.reverse("user-association") + + user = factory.UserFactory() + api_client.force_login(user=user) + response = api_client.post(url, data=payload, format="json") + + assert response.status_code == status.HTTP_204_NO_CONTENT + + other_user = factory.UserFactory() + api_client.force_login(user=other_user) + response = api_client.post(url, data=payload, format="json") + + assert response.status_code == status.HTTP_409_CONFLICT diff --git a/backend/tests/destination/integration/endpoints/test_user_details.py b/backend/tests/destination/integration/endpoints/test_user_details.py new file mode 100644 index 0000000..544a8c0 --- /dev/null +++ b/backend/tests/destination/integration/endpoints/test_user_details.py @@ -0,0 +1,26 @@ +import django.urls +import pytest +from rest_framework import status +from rest_framework import test + +from tests.destination.integration import factory + + +@pytest.mark.django_db() +def user_details_returns_username(api_client: test.APIClient): + user = factory.UserFactory() + api_client.force_login(user=user) + url = django.urls.reverse("user-details") + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + assert response.data["username"] == user.username + + +@pytest.mark.django_db() +def user_details_returns_401_when_not_authenticated(api_client: test.APIClient): + url = django.urls.reverse("user-details") + response = api_client.get(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert not response.has_header("Authenticated-User") diff --git a/backend/tests/destination/integration/endpoints/test_user_login.py b/backend/tests/destination/integration/endpoints/test_user_login.py new file mode 100644 index 0000000..f490585 --- /dev/null +++ b/backend/tests/destination/integration/endpoints/test_user_login.py @@ -0,0 +1,27 @@ +import pytest +from faker import Faker +from rest_framework import test + +from tests.common.integration.endpoints import login +from tests.destination.integration import factory + + +@pytest.mark.django_db() +def test_user_login_sets_cookies_with_default_values(api_client: test.APIClient): + login.user_login_sets_cookies_with_default_values(api_client) + + +@pytest.mark.django_db() +def test_user_login_forbidden_without_remote_user_header(api_client: test.APIClient): + login.user_login_forbidden_without_remote_user_header(api_client) + + +@pytest.mark.django_db() +def test_user_login_basic_auth(api_client: test.APIClient): + user = factory.UserFactory() + login.user_login_basic_auth(api_client, user) + + +@pytest.mark.django_db() +def test_user_login_removes_expired_sessions(api_client: test.APIClient, faker: Faker): + login.user_login_removes_expired_sessions(api_client, faker) diff --git a/backend/tests/destination/integration/factory.py b/backend/tests/destination/integration/factory.py new file mode 100644 index 0000000..84b28cf --- /dev/null +++ b/backend/tests/destination/integration/factory.py @@ -0,0 +1,161 @@ +import contextlib +import datetime +import hashlib +import io +from typing import ContextManager +from typing import Optional + +import django.contrib.auth +import django.utils.timezone +import factory +import faker.utils.decorators +from django.conf import settings + +from eurydice.common import minio +from eurydice.common.models import fields +from eurydice.destination.core import models as destination_models +from eurydice.destination.core.models import s3_upload_part +from eurydice.destination.utils import rehash +from tests import utils + + +class UserFactory(factory.django.DjangoModelFactory): + username = factory.Sequence(lambda n: f"{utils.fake.user_name()}_{n}") + first_name = factory.Faker("first_name") + last_name = factory.Faker("last_name") + password = factory.Faker("password") + is_superuser = False + + class Meta: + model = django.contrib.auth.get_user_model() + django_get_or_create = ("username",) + + +class UserProfileFactory(factory.django.DjangoModelFactory): + user = factory.SubFactory(UserFactory) + associated_user_profile_id = factory.Faker("uuid4") + + class Meta: + model = destination_models.UserProfile + + +class IncomingTransferableFactory(factory.django.DjangoModelFactory): + name = factory.Faker("file_name") + sha1 = factory.Faker("sha1", raw_output=True) + size = factory.Faker("pyint", min_value=0, max_value=settings.TRANSFERABLE_MAX_SIZE) + + _s3_bucket_name = factory.Faker( + "pystr", + min_chars=fields.S3BucketNameField.MIN_LENGTH, + max_chars=fields.S3BucketNameField.MAX_LENGTH, + ) + _s3_object_name = factory.Faker( + "pystr", + min_chars=fields.S3ObjectNameField.MIN_LENGTH, + max_chars=fields.S3ObjectNameField.MAX_LENGTH, + ) + _s3_upload_id = factory.Faker( + "pystr", + min_chars=destination_models.incoming_transferable.S3_UPLOAD_ID_LENGTH, + max_chars=destination_models.incoming_transferable.S3_UPLOAD_ID_LENGTH, + ) + _random_bytes = factory.Faker("binary", length=20) + _finished_at = factory.Faker( + "future_datetime", tzinfo=django.utils.timezone.get_current_timezone() + ) + + @factory.lazy_attribute + def bytes_received(self) -> int: + if self.state == destination_models.IncomingTransferableState.SUCCESS: + return self.size + + return factory.Faker("pyint", min_value=0, max_value=self.size - 1).evaluate( + None, None, {"locale": None} + ) + + @factory.lazy_attribute + @faker.utils.decorators.slugify + def s3_bucket_name(self) -> str: + return self._s3_bucket_name + + @factory.lazy_attribute + @faker.utils.decorators.slugify + def s3_object_name(self) -> str: + return self._s3_object_name + + @factory.lazy_attribute + def s3_upload_id(self) -> str: + return self._s3_upload_id + + @factory.lazy_attribute + def rehash_intermediary(self) -> bytes: + return rehash.sha1_to_bytes(hashlib.sha1(self._random_bytes)) # nosec: B303 + + user_profile = factory.SubFactory(UserProfileFactory) + + user_provided_meta = {"Metadata-Foo": "Bar"} + + state = factory.Faker( + "random_element", elements=destination_models.IncomingTransferableState + ) + + @factory.lazy_attribute + def finished_at(self) -> Optional[datetime.datetime]: + if self.state == destination_models.IncomingTransferableState.ONGOING: + return None + + return self._finished_at + + class Meta: + model = destination_models.IncomingTransferable + exclude = ( + "_s3_bucket_name", + "_s3_object_name", + "_s3_upload_id", + "_random_bytes", + "_finished_at", + ) + + +class S3UploadPartFactory(factory.django.DjangoModelFactory): + etag = factory.Faker("binary", length=s3_upload_part._S3_PART_ETAG_LENGTH) + part_number = factory.Sequence(lambda n: n + 1) + incoming_transferable = factory.SubFactory(IncomingTransferableFactory) + + class Meta: + model = destination_models.S3UploadPart + + +@contextlib.contextmanager +def s3_stored_incoming_transferable( + data: bytes, **kwargs +) -> ContextManager[destination_models.IncomingTransferable]: + obj = IncomingTransferableFactory(**kwargs) + + try: + minio.client.make_bucket(bucket_name=obj.s3_bucket_name) + minio.client.put_object( + bucket_name=obj.s3_bucket_name, + object_name=obj.s3_object_name, + data=io.BytesIO(data), + length=len( + data, + ), + ) + + yield obj + finally: + minio.client.remove_object( + bucket_name=obj.s3_bucket_name, object_name=obj.s3_object_name + ) + + minio.client.remove_bucket(bucket_name=obj.s3_bucket_name) + + +__all__ = ( + "UserFactory", + "UserProfileFactory", + "IncomingTransferableFactory", + "S3UploadPartFactory", + "s3_stored_incoming_transferable", +) diff --git a/backend/tests/destination/integration/receiver/__init__.py b/backend/tests/destination/integration/receiver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/integration/receiver/conftest.py b/backend/tests/destination/integration/receiver/conftest.py new file mode 100644 index 0000000..ad4dcc3 --- /dev/null +++ b/backend/tests/destination/integration/receiver/conftest.py @@ -0,0 +1,47 @@ +import contextlib +from typing import Iterator + +import pytest +from faker import Faker +from minio.error import S3Error + +from eurydice.common import minio +from eurydice.destination.core import models +from tests.destination.integration import factory as destination_factory + + +@pytest.fixture() +def ongoing_incoming_transferable( + faker: Faker, +) -> Iterator[models.IncomingTransferable]: + obj = destination_factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.ONGOING + ) + + try: + minio.client.make_bucket(bucket_name=obj.s3_bucket_name) + obj.s3_upload_id = minio.client._create_multipart_upload( + bucket_name=obj.s3_bucket_name, object_name=obj.s3_object_name, headers={} + ) + + obj.save() + + minio.client._upload_part( + bucket_name=obj.s3_bucket_name, + object_name=obj.s3_object_name, + data=faker.binary(length=1024), + headers={}, + upload_id=obj.s3_upload_id, + part_number=1, + ) + + yield obj + finally: + with contextlib.suppress(S3Error): + minio.client._abort_multipart_upload( + bucket_name=obj.s3_bucket_name, + object_name=obj.s3_object_name, + upload_id=obj.s3_upload_id, + ) + + minio.client.remove_bucket(bucket_name=obj.s3_bucket_name) diff --git a/backend/tests/destination/integration/receiver/packet_handler/__init__.py b/backend/tests/destination/integration/receiver/packet_handler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/integration/receiver/packet_handler/extractors/__init__.py b/backend/tests/destination/integration/receiver/packet_handler/extractors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/integration/receiver/packet_handler/extractors/test_history.py b/backend/tests/destination/integration/receiver/packet_handler/extractors/test_history.py new file mode 100644 index 0000000..ae4fc3c --- /dev/null +++ b/backend/tests/destination/integration/receiver/packet_handler/extractors/test_history.py @@ -0,0 +1,278 @@ +import hashlib +import logging +import uuid +from pathlib import Path + +import pytest +from django.conf import Settings + +from eurydice.common import protocol +from eurydice.destination.core import models +from eurydice.destination.core.models.incoming_transferable import ( + IncomingTransferableState, +) +from eurydice.destination.receiver.packet_handler.extractors import history +from eurydice.destination.storage import fs +from tests.common.integration import factory as common_factory +from tests.destination.integration import factory as destination_factory +from tests.destination.integration.utils import s3 as s3_utils + + +@pytest.mark.parametrize("state", set(IncomingTransferableState)) +def test_transferables_states_consistency(state: IncomingTransferableState): + assert state.is_final == (state != IncomingTransferableState.ONGOING) + + +@pytest.mark.django_db() +def test__list_ongoing_transferable_ids_success(): + ongoing_transferable = destination_factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.ONGOING + ) + + for state in models.IncomingTransferableState.get_final_states(): + destination_factory.IncomingTransferableFactory(state=state) + + transferable_ids = {ongoing_transferable.id} + assert history._list_ongoing_transferable_ids(transferable_ids) == transferable_ids + + +@pytest.mark.django_db() +def test__list_finished_transferable_ids_success(): + final_transferables = [ + destination_factory.IncomingTransferableFactory(state=state) + for state in models.IncomingTransferableState.get_final_states() + ] + + destination_factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.ONGOING + ) + + transferable_ids = {t.id for t in final_transferables} + assert history._list_finished_transferable_ids(transferable_ids) == transferable_ids + + +@pytest.mark.django_db() +def test__list_missed_transferable_ids_success(): + existing_transferable = destination_factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.ONGOING + ) + missed_transferable_ids = {uuid.uuid4() for _ in range(10)} + transferable_ids = missed_transferable_ids | {existing_transferable.id} + + assert ( + history._list_missed_transferable_ids(transferable_ids, set()) + == transferable_ids + ) + + +@pytest.mark.django_db() +def test__process_ongoing_transferables( + ongoing_incoming_transferable: models.IncomingTransferable, +): + assert s3_utils.multipart_upload_exists(ongoing_incoming_transferable) + + ongoing_transferable_ids = {ongoing_incoming_transferable.id} + history._process_ongoing_transferables(ongoing_transferable_ids) + assert ( + models.IncomingTransferable.objects.get().state + == models.IncomingTransferableState.ERROR + ) + + assert not s3_utils.multipart_upload_exists(ongoing_incoming_transferable) + + +@pytest.mark.django_db() +def test__process_missed_transferables(): + history_entry_map = history._HistoryEntryMap(common_factory.HistoryFactory()) + missed_transferable_ids = set(history_entry_map.keys()) + history._process_missed_transferables(missed_transferable_ids, history_entry_map) + + assert models.IncomingTransferable.objects.filter( + id__in=missed_transferable_ids, state=models.IncomingTransferableState.ERROR + ).count() == len(missed_transferable_ids) + + +@pytest.mark.django_db() +class TestOngoingHistoryExtractor: + def test_extract_empty_history_success(self, caplog: pytest.LogCaptureFixture): + caplog.set_level(logging.INFO) + packet = protocol.OnTheWirePacket(history=protocol.History(entries=[])) + history.OngoingHistoryExtractor().extract(packet) + assert not models.IncomingTransferable.objects.exists() + assert "History processed." in caplog.text + + def test_extract_history_success_nothing_to_process(self): + nb_transferables = 3 + + incoming_transferables = ( + destination_factory.IncomingTransferableFactory.create_batch( + size=nb_transferables, state=models.IncomingTransferableState.SUCCESS + ) + ) + packet = protocol.OnTheWirePacket( + history=protocol.History( + entries=[ + protocol.HistoryEntry( + transferable_id=t.id, + user_profile_id=uuid.uuid4(), + state=models.IncomingTransferableState.SUCCESS, + name="archive.zip", + sha1=hashlib.sha1(b"archive.zip").hexdigest(), + ) + for t in incoming_transferables + ] + ) + ) + history.OngoingHistoryExtractor().extract(packet) + + assert models.IncomingTransferable.objects.count() == nb_transferables + assert ( + models.IncomingTransferable.objects.filter( + state=models.IncomingTransferableState.SUCCESS + ).count() + == nb_transferables + ) + + def test_extract_history_success_process_ongoing( + self, ongoing_incoming_transferable: models.IncomingTransferable + ): + assert ( + ongoing_incoming_transferable.state + == models.IncomingTransferableState.ONGOING + ) + assert s3_utils.multipart_upload_exists(ongoing_incoming_transferable) + + packet = protocol.OnTheWirePacket( + history=protocol.History( + entries=[ + protocol.HistoryEntry( + transferable_id=ongoing_incoming_transferable.id, + user_profile_id=ongoing_incoming_transferable.user_profile.id, + state=models.IncomingTransferableState.SUCCESS, + name="archive.zip", + sha1=hashlib.sha1(b"archive.zip").hexdigest(), + ) + ] + ) + ) + history.OngoingHistoryExtractor().extract(packet) + ongoing_incoming_transferable.refresh_from_db() + assert ( + ongoing_incoming_transferable.state + == models.IncomingTransferableState.ERROR + ) + assert not s3_utils.multipart_upload_exists(ongoing_incoming_transferable) + + def test_extract_history_success_process_ongoing_with_filesystem_storage( + self, + settings: Settings, + tmp_path: Path, + ): + settings.TRANSFERABLE_STORAGE_DIR = tmp_path + settings.MINIO_ENABLED = False + ongoing_incoming_transferable = destination_factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.ONGOING + ) + fs.file_path(ongoing_incoming_transferable).parent.mkdir( + parents=True, exist_ok=True + ) + fs.file_path(ongoing_incoming_transferable).touch() + + assert ( + ongoing_incoming_transferable.state + == models.IncomingTransferableState.ONGOING + ) + assert fs.file_path(ongoing_incoming_transferable).exists() + + packet = protocol.OnTheWirePacket( + history=protocol.History( + entries=[ + protocol.HistoryEntry( + transferable_id=ongoing_incoming_transferable.id, + user_profile_id=ongoing_incoming_transferable.user_profile.id, + state=models.IncomingTransferableState.SUCCESS, + name="archive.zip", + sha1=hashlib.sha1(b"archive.zip").hexdigest(), + ) + ] + ) + ) + history.OngoingHistoryExtractor().extract(packet) + ongoing_incoming_transferable.refresh_from_db() + assert ( + ongoing_incoming_transferable.state + == models.IncomingTransferableState.ERROR + ) + assert not fs.file_path(ongoing_incoming_transferable).exists() + + def test_extract_history_success_process_missed(self): + assert not models.IncomingTransferable.objects.exists() + + packet = protocol.OnTheWirePacket( + history=protocol.History( + entries=[ + protocol.HistoryEntry( + transferable_id=uuid.uuid4(), + user_profile_id=uuid.uuid4(), + state=models.IncomingTransferableState.SUCCESS, + name="archive.zip", + sha1=hashlib.sha1(b"archive.zip").hexdigest(), + user_provided_meta={"Metadata-Foo": "Bar"}, + ) + ] + ) + ) + history.OngoingHistoryExtractor().extract(packet) + + assert models.IncomingTransferable.objects.count() == 1 + incoming_transferable = models.IncomingTransferable.objects.get() + assert incoming_transferable.id == packet.history.entries[0].transferable_id + assert incoming_transferable.name == packet.history.entries[0].name + assert bytes(incoming_transferable.sha1) == packet.history.entries[0].sha1 + assert ( + incoming_transferable.user_profile.associated_user_profile_id + == packet.history.entries[0].user_profile_id + ) + assert incoming_transferable.state == models.IncomingTransferableState.ERROR + assert incoming_transferable.user_provided_meta == {"Metadata-Foo": "Bar"} + + def test_extract_history_success_no_history(self): + packet = protocol.OnTheWirePacket() + history.OngoingHistoryExtractor().extract(packet) + + def test_extract_history_process_cleaned_entry(self): + """ + Tests that an IncomingTransferable does not go to status ERROR if its status + is set to EXPIRED by the s3cleaner + """ + incoming_transferable = destination_factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.SUCCESS + ) + + # simulate s3cleaner + incoming_transferable.state = IncomingTransferableState.EXPIRED + incoming_transferable.save() + + packet = protocol.OnTheWirePacket( + history=protocol.History( + entries=[ + protocol.HistoryEntry( + transferable_id=incoming_transferable.id, + user_profile_id=uuid.uuid4(), + state=models.IncomingTransferableState.SUCCESS, + name="archive.zip", + sha1=hashlib.sha1(b"archive.zip").hexdigest(), + ) + ] + ) + ) + history.OngoingHistoryExtractor().extract(packet) + + # history didn't make the transferable go bad + assert models.IncomingTransferable.objects.count() == 1 + assert ( + models.IncomingTransferable.objects.filter( + state=models.IncomingTransferableState.EXPIRED + ).count() + == 1 + ) diff --git a/backend/tests/destination/integration/receiver/packet_handler/extractors/test_transferable_range.py b/backend/tests/destination/integration/receiver/packet_handler/extractors/test_transferable_range.py new file mode 100644 index 0000000..d81518f --- /dev/null +++ b/backend/tests/destination/integration/receiver/packet_handler/extractors/test_transferable_range.py @@ -0,0 +1,478 @@ +import hashlib +import logging +from typing import Optional + +import humanfriendly as hf +import pytest +from faker import Faker + +from eurydice.common import minio +from eurydice.destination.core import models +from eurydice.destination.receiver.packet_handler import extractors +from eurydice.destination.utils import rehash +from tests.common.integration import factory as common_factory +from tests.destination.integration import factory as destination_factory +from tests.destination.integration.utils import s3 as s3_utils + + +@pytest.mark.django_db() +def test_transferable_range_extractor_success(): + extractor = extractors.TransferableRangeExtractor() + + first_transferable_range_data = b"0" * hf.parse_size("5MiB") + + transferable_range = common_factory.TransferableRangeFactory( + transferable=common_factory.TransferableFactory(sha1=None, size=None), + byte_offset=0, + data=first_transferable_range_data, + is_last=False, + ) + + packet = common_factory.OnTheWirePacketFactory( + transferable_ranges=[transferable_range] + ) + + extractor.extract(packet) + + all_transferables = models.IncomingTransferable.objects.all() + + assert all_transferables.count() == 1 + + queried_transferable = all_transferables.first() + + assert queried_transferable.id == transferable_range.transferable.id + assert queried_transferable.state == models.IncomingTransferableState.ONGOING + assert queried_transferable.bytes_received == len(transferable_range.data) + assert queried_transferable.size is None + assert bytes(queried_transferable.rehash_intermediary) == rehash.sha1_to_bytes( + hashlib.sha1(transferable_range.data) + ) + assert queried_transferable.finished_at is None + assert queried_transferable.sha1 is None + + assert s3_utils.multipart_upload_exists(queried_transferable) + + final_transferable_range_data = b"0" * hf.parse_size("5KiB") + final_transferable_sha1 = hashlib.sha1( + transferable_range.data + final_transferable_range_data + ) + final_transferable_size = len(first_transferable_range_data) + len( + final_transferable_range_data + ) + + another_transferable_range = common_factory.TransferableRangeFactory( + transferable=common_factory.TransferableFactory( + id=queried_transferable.id, + user_profile_id=( + queried_transferable.user_profile.associated_user_profile_id + ), + sha1=final_transferable_sha1.digest(), + size=final_transferable_size, + ), + data=final_transferable_range_data, + is_last=True, + byte_offset=queried_transferable.bytes_received, + ) + + packet = common_factory.OnTheWirePacketFactory( + transferable_ranges=[another_transferable_range] + ) + + extractor.extract(packet) + + all_transferables = models.IncomingTransferable.objects.all() + + assert all_transferables.count() == 1 + + queried_transferable = all_transferables.first() + + assert queried_transferable.id == transferable_range.transferable.id + assert queried_transferable.state == models.IncomingTransferableState.SUCCESS + assert queried_transferable.bytes_received == final_transferable_size + assert queried_transferable.size == final_transferable_size + assert bytes(queried_transferable.rehash_intermediary) == rehash.sha1_to_bytes( + final_transferable_sha1 + ) + assert queried_transferable.finished_at is not None + assert bytes(queried_transferable.sha1) == final_transferable_sha1.digest() + + response = None + try: + response = minio.client.get_object( + bucket_name=queried_transferable.s3_bucket_name, + object_name=queried_transferable.s3_object_name, + ) + data = response.read() + finally: + if response: + response.close() + response.release_conn() + + assert data == first_transferable_range_data + final_transferable_range_data + + +@pytest.mark.django_db() +@pytest.mark.parametrize("transferable_range_size", ["1B", "1KiB", "4.99KiB"]) +def test_transferable_range_extractor_success_small_transferable_range( + transferable_range_size: str, +): + extractor = extractors.TransferableRangeExtractor() + + transferable_range_size = hf.parse_size(transferable_range_size) + transferable_range_data = b"0" * transferable_range_size + transferable_range_digest = hashlib.sha1(transferable_range_data) + + transferable_range = common_factory.TransferableRangeFactory( + transferable=common_factory.TransferableFactory( + sha1=transferable_range_digest.digest(), + size=transferable_range_size, + ), + byte_offset=0, + data=transferable_range_data, + is_last=True, + ) + + packet = common_factory.OnTheWirePacketFactory( + transferable_ranges=[transferable_range] + ) + + extractor.extract(packet) + + all_transferables = models.IncomingTransferable.objects.all() + + assert all_transferables.count() == 1 + + queried_transferable = all_transferables.first() + + assert queried_transferable.id == transferable_range.transferable.id + assert queried_transferable.state == models.IncomingTransferableState.SUCCESS + assert queried_transferable.bytes_received == transferable_range_size + assert queried_transferable.size == transferable_range_size + assert bytes(queried_transferable.rehash_intermediary) == rehash.sha1_to_bytes( + transferable_range_digest + ) + assert queried_transferable.finished_at is not None + assert bytes(queried_transferable.sha1) == transferable_range_digest.digest() + assert queried_transferable.s3_upload_parts.count() == 0 + + response = None + try: + response = minio.client.get_object( + bucket_name=queried_transferable.s3_bucket_name, + object_name=queried_transferable.s3_object_name, + ) + data = response.read() + finally: + if response: + response.close() + response.release_conn() + + assert data == transferable_range_data + + +@pytest.mark.django_db() +def test_transferable_range_extractor_missed_transferable_range(faker: Faker): + extractor = extractors.TransferableRangeExtractor() + + transferable_range = common_factory.TransferableRangeFactory( + byte_offset=faker.pyint(min_value=2) + ) + packet = common_factory.OnTheWirePacketFactory( + transferable_ranges=[transferable_range] + ) + + extractor.extract(packet) + + queried_transferable = models.IncomingTransferable.objects.get( + id=transferable_range.transferable.id + ) + + assert queried_transferable.state == models.IncomingTransferableState.ERROR + assert queried_transferable.finished_at is not None + + +@pytest.mark.django_db() +def test_transferable_range_transferable_already_marked_as_error( + caplog: pytest.LogCaptureFixture, +): + incoming_transferable = destination_factory.IncomingTransferableFactory( + bytes_received=1024, size=2048, state=models.IncomingTransferableState.ERROR + ) + transferable_range = common_factory.TransferableRangeFactory( + transferable=common_factory.TransferableFactory(id=incoming_transferable.id), + byte_offset=incoming_transferable.bytes_received, + ) + packet = common_factory.OnTheWirePacketFactory( + transferable_ranges=[transferable_range] + ) + + extractor = extractors.TransferableRangeExtractor() + extractor.extract(packet) + + assert caplog.messages[0] == f"Extracting data for {incoming_transferable.id}" + assert caplog.messages[1] == ( + f"IncomingTransferable {incoming_transferable.id} has state ERROR. " + "Ignoring the associated transferable range received." + ) + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + ("actual_transferable_size", "reported_transferable_size"), + [ + ("4KiB", "3KiB"), + ("3KiB", "4KiB"), + ("10KiB", "15KiB"), + ("15KiB", "10KiB"), + ], +) +def test_transferable_range_extractor_new_transferable_size_mismatch( + actual_transferable_size: str, + reported_transferable_size: Optional[str], + caplog: pytest.LogCaptureFixture, +): + caplog.set_level(logging.ERROR) + extractor = extractors.TransferableRangeExtractor() + + transferable_range_size = hf.parse_size(actual_transferable_size) + transferable_range_data = b"0" * transferable_range_size + transferable_range_digest = hashlib.sha1(transferable_range_data) + + transferable_size = hf.parse_size(reported_transferable_size) + + transferable_range = common_factory.TransferableRangeFactory( + transferable=common_factory.TransferableFactory( + sha1=transferable_range_digest.digest(), size=transferable_size + ), + byte_offset=0, + data=transferable_range_data, + is_last=True, + ) + + packet = common_factory.OnTheWirePacketFactory( + transferable_ranges=[transferable_range] + ) + + extractor.extract(packet) + + assert ( + f"Received {hf.format_size(len(transferable_range_data))}, " + f"expected {hf.format_size(transferable_size)} " + f"for Transferable {transferable_range.transferable.id}" + ) in caplog.text + + queried_transferable = models.IncomingTransferable.objects.get( + id=transferable_range.transferable.id + ) + + assert queried_transferable.state == models.IncomingTransferableState.ERROR + assert queried_transferable.finished_at is not None + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + "reported_sha1", + [ + b"\xc6\xa8\x82F\x0c\xadFrU\xaf\x1dx\xda\r\xea?\xdc\xd3V\x93", + ], +) +def test_transferable_range_extractor_new_transferable_digest_mismatch( + reported_sha1: bytes, caplog: pytest.LogCaptureFixture, faker: Faker +): + caplog.set_level(logging.ERROR) + extractor = extractors.TransferableRangeExtractor() + + transferable_range_data = faker.binary(10) + + transferable_range = common_factory.TransferableRangeFactory( + transferable=common_factory.TransferableFactory( + sha1=reported_sha1, size=len(transferable_range_data) + ), + byte_offset=0, + data=transferable_range_data, + is_last=True, + ) + + packet = common_factory.OnTheWirePacketFactory( + transferable_ranges=[transferable_range] + ) + + extractor.extract(packet) + + assert f"expected {reported_sha1.hex()}" in caplog.text + assert str(transferable_range.transferable.id) in caplog.text + + queried_transferable = models.IncomingTransferable.objects.get( + id=transferable_range.transferable.id + ) + + assert queried_transferable.state == models.IncomingTransferableState.ERROR + assert queried_transferable.finished_at is not None + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + "reported_sha1", + [b"\x03\xd7\x03,@No\x92\x8b\\\xf2\xa5\xb5\xb4\x06\x02\xd0\t\x19\xc9"], +) +def test_transferable_range_extractor_existing_transferable_digest_mismatch( + reported_sha1: bytes, +): + extractor = extractors.TransferableRangeExtractor() + + first_transferable_range_data = b"0" * hf.parse_size("5KiB") + + transferable_range = common_factory.TransferableRangeFactory( + transferable=common_factory.TransferableFactory( + sha1=None, size=2 * hf.parse_size("5KiB") + ), + byte_offset=0, + data=first_transferable_range_data, + is_last=False, + ) + + packet = common_factory.OnTheWirePacketFactory( + transferable_ranges=[transferable_range] + ) + + extractor.extract(packet) + + all_transferables = models.IncomingTransferable.objects.all() + + assert all_transferables.count() == 1 + + queried_transferable = all_transferables.first() + + assert queried_transferable.id == transferable_range.transferable.id + assert queried_transferable.state == models.IncomingTransferableState.ONGOING + assert queried_transferable.bytes_received == len(transferable_range.data) + assert queried_transferable.size == 2 * hf.parse_size("5KiB") + assert bytes(queried_transferable.rehash_intermediary) == rehash.sha1_to_bytes( + hashlib.sha1(transferable_range.data) + ) + assert queried_transferable.finished_at is None + assert queried_transferable.sha1 is None + + assert s3_utils.multipart_upload_exists(queried_transferable) + + final_transferable_range_data = b"0" * hf.parse_size("5KiB") + + another_transferable_range = common_factory.TransferableRangeFactory( + transferable=common_factory.TransferableFactory( + id=queried_transferable.id, + user_profile_id=( + queried_transferable.user_profile.associated_user_profile_id + ), + sha1=reported_sha1, + size=2 * hf.parse_size("5KiB"), + ), + data=final_transferable_range_data, + is_last=True, + byte_offset=queried_transferable.bytes_received, + ) + + packet = common_factory.OnTheWirePacketFactory( + transferable_ranges=[another_transferable_range] + ) + + extractor.extract(packet) + + all_transferables = models.IncomingTransferable.objects.all() + + assert all_transferables.count() == 1 + + queried_transferable = all_transferables.first() + + assert queried_transferable.state == models.IncomingTransferableState.ERROR + assert queried_transferable.finished_at is not None + + assert not s3_utils.multipart_upload_exists(queried_transferable) + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + ("actual_transferable_size", "reported_transferable_size"), + [ + ("4KiB", "3KiB"), + ("3KiB", "4KiB"), + ("6KiB", "7KiB"), + ("7KiB", "6KiB"), + ], +) +def test_transferable_range_extractor_existing_transferable_size_mismatch( + actual_transferable_size: str, + reported_transferable_size: str, +): + extractor = extractors.TransferableRangeExtractor() + + first_transferable_range_size = hf.parse_size(actual_transferable_size) // 2 + first_transferable_range_data = b"0" * first_transferable_range_size + + transferable_range = common_factory.TransferableRangeFactory( + transferable=common_factory.TransferableFactory(sha1=None, size=None), + byte_offset=0, + data=first_transferable_range_data, + is_last=False, + ) + + packet = common_factory.OnTheWirePacketFactory( + transferable_ranges=[transferable_range] + ) + + extractor.extract(packet) + + all_transferables = models.IncomingTransferable.objects.all() + + assert all_transferables.count() == 1 + + queried_transferable = all_transferables.first() + + assert queried_transferable.id == transferable_range.transferable.id + assert queried_transferable.state == models.IncomingTransferableState.ONGOING + assert queried_transferable.bytes_received == len(transferable_range.data) + assert bytes(queried_transferable.rehash_intermediary) == rehash.sha1_to_bytes( + hashlib.sha1(transferable_range.data) + ) + assert queried_transferable.finished_at is None + assert queried_transferable.sha1 is None + + assert s3_utils.multipart_upload_exists(queried_transferable) + + reported_final_transferable_size = hf.parse_size(reported_transferable_size) + final_transferable_range_size = ( + hf.parse_size(actual_transferable_size) - first_transferable_range_size + ) + final_transferable_range_data = b"0" * final_transferable_range_size + + another_transferable_range = common_factory.TransferableRangeFactory( + transferable=common_factory.TransferableFactory( + id=queried_transferable.id, + user_profile_id=( + queried_transferable.user_profile.associated_user_profile_id + ), + sha1=hashlib.sha1( + first_transferable_range_data + final_transferable_range_data + ).digest(), + size=reported_final_transferable_size, + ), + data=final_transferable_range_data, + is_last=True, + byte_offset=queried_transferable.bytes_received, + ) + + packet = common_factory.OnTheWirePacketFactory( + transferable_ranges=[another_transferable_range] + ) + + extractor.extract(packet) + + all_transferables = models.IncomingTransferable.objects.all() + + assert all_transferables.count() == 1 + + queried_transferable = all_transferables.first() + + assert queried_transferable.state == models.IncomingTransferableState.ERROR + assert queried_transferable.finished_at is not None + + assert not s3_utils.multipart_upload_exists(queried_transferable) diff --git a/backend/tests/destination/integration/receiver/packet_handler/extractors/test_transferable_revocation.py b/backend/tests/destination/integration/receiver/packet_handler/extractors/test_transferable_revocation.py new file mode 100644 index 0000000..006787a --- /dev/null +++ b/backend/tests/destination/integration/receiver/packet_handler/extractors/test_transferable_revocation.py @@ -0,0 +1,166 @@ +import hashlib +import logging + +import pytest +from faker import Faker + +from eurydice.common import enums +from eurydice.common import protocol +from eurydice.destination.core import models +from eurydice.destination.receiver.packet_handler import extractors +from eurydice.destination.receiver.packet_handler.extractors import ( + transferable_revocation, +) +from tests.common.integration import factory as common_factory +from tests.destination.integration import factory as destination_factory +from tests.destination.integration.utils import s3 as s3_utils + + +@pytest.mark.django_db() +@pytest.mark.parametrize("user_profile_exists", [True, False]) +def test_create_failed( + user_profile_exists: bool, faker: Faker, caplog: pytest.LogCaptureFixture +): + caplog.set_level(logging.INFO) + + assert not models.IncomingTransferable.objects.exists() + + associated_user_profile_id = faker.uuid4(cast_to=None) + + if user_profile_exists: + user_profile = models.UserProfile.objects.create( + associated_user_profile_id=associated_user_profile_id + ) + + transferable_id = faker.uuid4(cast_to=None) + transferable_name = "archive.zip" + transferable_sha1 = hashlib.sha1(b"archive.zip").hexdigest() + transferable_revocation._create_revoked_transferable( + protocol.TransferableRevocation( + transferable_id=transferable_id, + user_profile_id=associated_user_profile_id, + reason=enums.TransferableRevocationReason.USER_CANCELED, + transferable_name=transferable_name, + transferable_sha1=transferable_sha1, + ) + ) + + incoming_transferable = models.IncomingTransferable.objects.get(id=transferable_id) + assert incoming_transferable.state == models.IncomingTransferableState.REVOKED + assert incoming_transferable.created_at == incoming_transferable.finished_at + assert incoming_transferable.name == transferable_name + assert bytes(incoming_transferable.sha1).decode("utf-8") == transferable_sha1 + + if user_profile_exists: + assert incoming_transferable.user_profile == user_profile + else: + assert ( + incoming_transferable.user_profile.associated_user_profile_id + == associated_user_profile_id + ) + + +@pytest.mark.django_db() +def test_extract_transferable_revocation_success_no_transferable( + faker: Faker, + caplog: pytest.LogCaptureFixture, +): + assert not models.IncomingTransferable.objects.exists() + + transferable_id = faker.uuid4(cast_to=None) + transferable_name = "archive.zip" + transferable_sha1 = hashlib.sha1(b"archive.zip").hexdigest() + packet = protocol.OnTheWirePacket( + transferable_revocations=[ + common_factory.TransferableRevocationFactory( + transferable_id=transferable_id, + transferable_name=transferable_name, + transferable_sha1=transferable_sha1, + ) + ] + ) + extractors.TransferableRevocationExtractor().extract(packet) + + transferable = models.IncomingTransferable.objects.get(id=transferable_id) + assert transferable.state == models.IncomingTransferableState.REVOKED + assert transferable.name == transferable_name + assert bytes(transferable.sha1).decode("utf-8") == transferable_sha1 + assert [ + f"The IncomingTransferable {transferable_id} has been marked " + "as REVOKED and its data removed from the storage (if it had any)." + ] == caplog.messages + + +@pytest.mark.django_db() +def test_extract_transferable_revocation_success_ongoing_transferable( + ongoing_incoming_transferable: models.IncomingTransferable, + caplog: pytest.LogCaptureFixture, +): + assert ( + ongoing_incoming_transferable.state == models.IncomingTransferableState.ONGOING + ) + + packet = protocol.OnTheWirePacket( + transferable_revocations=[ + common_factory.TransferableRevocationFactory( + transferable_id=ongoing_incoming_transferable.id + ) + ] + ) + + extractors.TransferableRevocationExtractor().extract(packet) + ongoing_incoming_transferable.refresh_from_db() + assert ( + ongoing_incoming_transferable.state == models.IncomingTransferableState.REVOKED + ) + assert not s3_utils.multipart_upload_exists(ongoing_incoming_transferable) + assert [ + f"The IncomingTransferable {ongoing_incoming_transferable.id} has been marked " + "as REVOKED and its data removed from the storage (if it had any)." + ] == caplog.messages + + +@pytest.mark.django_db() +@pytest.mark.parametrize("state", models.IncomingTransferableState.get_final_states()) +def test_extract_transferable_revocation_error_invalid_transferable_state( + state: models.IncomingTransferableState, + caplog: pytest.LogCaptureFixture, +): + incoming_transferable = destination_factory.IncomingTransferableFactory(state=state) + packet = protocol.OnTheWirePacket( + transferable_revocations=[ + common_factory.TransferableRevocationFactory( + transferable_id=incoming_transferable.id + ) + ] + ) + + extractors.TransferableRevocationExtractor().extract(packet) + assert [ + f"The IncomingTransferable {incoming_transferable.id} cannot be revoked as " + f"its state is {state.value}. " + f"Only {models.IncomingTransferableState.ONGOING.value} transferables " + f"can be revoked." + ] == caplog.messages + + +@pytest.mark.django_db() +def test_extract_transferable_revocation_error_s3( + caplog: pytest.LogCaptureFixture, +): + incoming_transferable = destination_factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.ONGOING + ) + packet = protocol.OnTheWirePacket( + transferable_revocations=[ + common_factory.TransferableRevocationFactory( + transferable_id=incoming_transferable.id + ) + ] + ) + + extractors.TransferableRevocationExtractor().extract(packet) + + assert "Cannot process revocation" in caplog.text + incoming_transferable.refresh_from_db() + assert incoming_transferable.state == models.IncomingTransferableState.ONGOING diff --git a/backend/tests/destination/integration/receiver/test_packet_receiver.py b/backend/tests/destination/integration/receiver/test_packet_receiver.py new file mode 100644 index 0000000..bfd0a52 --- /dev/null +++ b/backend/tests/destination/integration/receiver/test_packet_receiver.py @@ -0,0 +1,124 @@ +import socket +import time + +import pytest +from django.test import override_settings + +from eurydice.destination.receiver import packet_receiver +from tests.common.integration.factory import protocol as protocol_factory + + +@override_settings(PACKET_RECEIVER_PORT=0) +def test_packet_receiver_success(): + receiver = packet_receiver.PacketReceiver() + receiver.start() + + receiver_port = receiver._receiver_thread._server.server_address[1] + + packet = protocol_factory.OnTheWirePacketFactory() + with socket.create_connection(("127.0.0.1", receiver_port)) as conn: + conn.sendall(packet.to_bytes()) + + assert receiver.receive() == packet + + receiver.stop() + assert not receiver._receiver_thread.is_alive() + assert receiver._queue.empty() + + +@override_settings(PACKET_RECEIVER_PORT=0) +def test_packet_receiver_context_manager_success(): + with packet_receiver.PacketReceiver() as receiver: + packet = protocol_factory.OnTheWirePacketFactory() + + receiver_port = receiver._receiver_thread._server.server_address[1] + with socket.create_connection(("127.0.0.1", receiver_port)) as conn: + conn.sendall(packet.to_bytes()) + + assert receiver.receive() == packet + assert receiver._queue.empty() + + +@override_settings(PACKET_RECEIVER_PORT=0, RECEIVER_BUFFER_MAX_ITEMS=12) +def test_packet_receiver_receive_multiple_successively_success(): + packets = [protocol_factory.OnTheWirePacketFactory() for _ in range(10)] + + with packet_receiver.PacketReceiver() as receiver: + receiver_port = receiver._receiver_thread._server.server_address[1] + for packet in packets: + with socket.create_connection(("127.0.0.1", receiver_port)) as conn: + conn.sendall(packet.to_bytes()) + + assert packet == receiver.receive() + + assert receiver._queue.empty() + + +@override_settings(PACKET_RECEIVER_PORT=0, RECEIVER_BUFFER_MAX_ITEMS=4) +def test_packet_receiver_receive_multiple_overflow(caplog: pytest.LogCaptureFixture): + packets = [protocol_factory.OnTheWirePacketFactory() for _ in range(10)] + + expected_hits = 4 + expected_misses = len(packets) - expected_hits + expected_log_msg = ( + "Dropped Transferable - Receiver received data " + "while processing queue is at full capacity" + ) + + with packet_receiver.PacketReceiver() as receiver: + receiver_port = receiver._receiver_thread._server.server_address[1] + for packet in packets: + with socket.create_connection(("127.0.0.1", receiver_port)) as conn: + conn.sendall(packet.to_bytes()) + + # wait for the receiving thread to read all data + time.sleep(0.1) + + for nb_received, packet in enumerate(packets): + if nb_received < expected_hits: + assert packet == receiver.receive(block=False) + else: + with pytest.raises(packet_receiver.NothingToReceive): + receiver.receive(block=False) + + assert len(caplog.messages) == expected_misses + for logmsg in caplog.messages: + assert expected_log_msg == logmsg + + assert receiver._queue.empty() + + +@override_settings(PACKET_RECEIVER_PORT=0, RECEIVER_BUFFER_MAX_ITEMS=12) +def test_packet_receiver_receive_batch_success(): + packets = [protocol_factory.OnTheWirePacketFactory() for _ in range(10)] + + with packet_receiver.PacketReceiver() as receiver: + receiver_port = receiver._receiver_thread._server.server_address[1] + for packet in packets: + with socket.create_connection(("127.0.0.1", receiver_port)) as conn: + conn.sendall(packet.to_bytes()) + + for packet in packets: + assert packet == receiver.receive() + + assert receiver._queue.empty() + + +@override_settings(PACKET_RECEIVER_PORT=0) +def test_packet_receiver_error_raise_ReceptionError(): # noqa: N802 + with packet_receiver.PacketReceiver() as receiver: + receiver_port = receiver._receiver_thread._server.server_address[1] + with socket.create_connection(("127.0.0.1", receiver_port)) as conn: + conn.sendall(b"hello, world") + + with pytest.raises(packet_receiver.ReceptionError): + receiver.receive() + + +@pytest.mark.parametrize(("block", "timeout"), [(False, 0), (True, 0.01)]) +def test_packet_receiver_error_raise_NothingToReceive( # noqa: N802 + block: bool, timeout: float +): + with packet_receiver.PacketReceiver() as receiver: # noqa: SIM117 + with pytest.raises(packet_receiver.NothingToReceive): + receiver.receive(block, timeout) diff --git a/backend/tests/destination/integration/receiver/test_receiver.py b/backend/tests/destination/integration/receiver/test_receiver.py new file mode 100644 index 0000000..b89a3cd --- /dev/null +++ b/backend/tests/destination/integration/receiver/test_receiver.py @@ -0,0 +1,38 @@ +import os +import subprocess +import sys + +import pytest +from django.conf import settings + +from eurydice.destination import receiver + + +@pytest.mark.django_db() +def test_start_and_graceful_shutdown(): + with subprocess.Popen( + [sys.executable, "-m", receiver.__name__], + cwd=os.path.dirname(settings.BASE_DIR), + stderr=subprocess.PIPE, + env={ + "DB_NAME": settings.DATABASES["default"]["NAME"], + "DB_USER": settings.DATABASES["default"]["USER"], + "DB_PASSWORD": settings.DATABASES["default"]["PASSWORD"], + "DB_HOST": settings.DATABASES["default"]["HOST"], + "DB_PORT": str(settings.DATABASES["default"]["PORT"]), + "MINIO_ENDPOINT": settings.MINIO_ENDPOINT, + "MINIO_ACCESS_KEY": settings.MINIO_ACCESS_KEY, + "MINIO_SECRET_KEY": settings.MINIO_SECRET_KEY, + "MINIO_BUCKET_NAME": settings.MINIO_BUCKET_NAME, + "TRANSFERABLE_STORAGE_DIR": settings.TRANSFERABLE_STORAGE_DIR, + "PACKET_RECEIVER_HOST": settings.PACKET_RECEIVER_HOST, + "PACKET_RECEIVER_PORT": str(settings.PACKET_RECEIVER_PORT), + "USER_ASSOCIATION_TOKEN_SECRET_KEY": settings.USER_ASSOCIATION_TOKEN_SECRET_KEY, # noqa: E501 + }, + ) as proc: + while b"Ready to receive OnTheWirePackets" not in proc.stderr.readline(): + pass + + proc.terminate() + return_code = proc.wait() + assert return_code == 0 diff --git a/backend/tests/destination/integration/utils/__init__.py b/backend/tests/destination/integration/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/integration/utils/s3.py b/backend/tests/destination/integration/utils/s3.py new file mode 100644 index 0000000..cfe8e73 --- /dev/null +++ b/backend/tests/destination/integration/utils/s3.py @@ -0,0 +1,22 @@ +from minio.error import S3Error + +from eurydice.common import minio +from eurydice.destination.core import models + + +def multipart_upload_exists(incoming_transferable: models.IncomingTransferable) -> bool: + try: + minio.client._list_parts( + bucket_name=incoming_transferable.s3_bucket_name, + object_name=incoming_transferable.s3_object_name, + upload_id=incoming_transferable.s3_upload_id, + ) + except S3Error as e: + if e.code == "NoSuchUpload": + return False + raise + + return True + + +__all__ = ("multipart_upload_exists",) diff --git a/backend/tests/destination/unit/__init__.py b/backend/tests/destination/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/unit/api/__init__.py b/backend/tests/destination/unit/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/unit/api/test_permissions.py b/backend/tests/destination/unit/api/test_permissions.py new file mode 100644 index 0000000..1da0694 --- /dev/null +++ b/backend/tests/destination/unit/api/test_permissions.py @@ -0,0 +1,58 @@ +from unittest import mock + +import pytest +from rest_framework import request +from rest_framework import views + +from eurydice.destination.api import permissions +from tests.destination.integration import factory + + +@pytest.mark.django_db() +def test__is_associated_user_when_user_is_associated(): + user = factory.UserFactory() + factory.UserProfileFactory(user=user) + assert permissions._is_associated_user(user) is True + + +@pytest.mark.django_db() +def test__is_associated_user_when_user_is_not_associated(): + user = factory.UserFactory() + assert permissions._is_associated_user(user) is False + + +@pytest.mark.parametrize( + ("is_authenticated", "is_associated", "expected_result"), + [ + (False, False, False), + (True, False, False), + (False, True, False), + (True, True, True), + ], +) +@mock.patch( + "eurydice.destination.api.permissions.permissions.IsAuthenticated.has_permission" +) +@mock.patch("eurydice.destination.api.permissions._is_associated_user") +def test_is_associated_user_has_permission( + mocked_is_authenticated_has_permission: mock.Mock, + mocked__is_associated_user: mock.Mock, + is_authenticated: bool, + is_associated: bool, + expected_result: bool, +): + mocked_is_authenticated_has_permission.return_value = is_authenticated + + mocked__is_associated_user.return_value = is_associated + + mocked_is_associated_user = mock.Mock(spec=permissions.IsAssociatedUser) + + mocked_request = mock.Mock(spec=request.Request) + mocked_view = mock.Mock(spec=views.APIView) + + assert ( + permissions.IsAssociatedUser.has_permission( + mocked_is_associated_user, mocked_request, mocked_view + ) + is expected_result + ) diff --git a/backend/tests/destination/unit/cleaning/__init__.py b/backend/tests/destination/unit/cleaning/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/unit/cleaning/test_main.py b/backend/tests/destination/unit/cleaning/test_main.py new file mode 100644 index 0000000..1d45620 --- /dev/null +++ b/backend/tests/destination/unit/cleaning/test_main.py @@ -0,0 +1,24 @@ +import datetime +from typing import Optional + +import pytest +from django.conf import settings +from django.utils import timezone + +from eurydice.destination.cleaning.s3remover.s3remover import DestinationS3Remover + + +@pytest.mark.parametrize( + ("last_clean_at", "expected"), + [ + (None, True), + (timezone.now() - settings.S3REMOVER_RUN_EVERY, True), + (timezone.now() + datetime.timedelta(minutes=60), False), + ], +) +def test_should_clean_success( + last_clean_at: Optional[datetime.datetime], expected: bool +): + s3remover = DestinationS3Remover() + s3remover._last_run_at = last_clean_at + assert s3remover._should_run() is expected diff --git a/backend/tests/destination/unit/receiver/__init__.py b/backend/tests/destination/unit/receiver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/unit/receiver/packet_handler/__init__.py b/backend/tests/destination/unit/receiver/packet_handler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/unit/receiver/packet_handler/extractors/__init__.py b/backend/tests/destination/unit/receiver/packet_handler/extractors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/unit/receiver/packet_handler/extractors/test_history.py b/backend/tests/destination/unit/receiver/packet_handler/extractors/test_history.py new file mode 100644 index 0000000..b27dfc0 --- /dev/null +++ b/backend/tests/destination/unit/receiver/packet_handler/extractors/test_history.py @@ -0,0 +1,26 @@ +import uuid +from typing import cast + +import pytest + +from eurydice.common import protocol +from eurydice.destination.receiver.packet_handler.extractors import history + + +class MockedHistoryEntry: # noqa: SIM119 + def __init__(self): + self.transferable_id = uuid.uuid4() + + +class MockedHistory: # noqa: SIM119 + def __init__(self, nb_entries: int): + self.entries = [MockedHistoryEntry() for _ in range(nb_entries)] + + +class Test_HistoryEntryMap: # noqa: N801 + @pytest.mark.parametrize("nb_entries", range(3)) + def test_constructor_success(self, nb_entries: int): + hist = cast(protocol.History, MockedHistory(nb_entries=nb_entries)) + assert history._HistoryEntryMap(hist) == { + e.transferable_id: e for e in hist.entries + } diff --git a/backend/tests/destination/unit/receiver/packet_handler/extractors/test_transferable_range.py b/backend/tests/destination/unit/receiver/packet_handler/extractors/test_transferable_range.py new file mode 100644 index 0000000..64f9660 --- /dev/null +++ b/backend/tests/destination/unit/receiver/packet_handler/extractors/test_transferable_range.py @@ -0,0 +1,266 @@ +import functools +import hashlib +import logging +from typing import Optional +from unittest import mock + +import pytest +from faker import Faker + +import eurydice.destination.core.models as models +import eurydice.destination.receiver.packet_handler.extractors.transferable_range as transferable_range # noqa: E501 +import eurydice.destination.utils.rehash as rehash +import tests.common.integration.factory as protocol_factory +import tests.destination.integration.factory as factory +from eurydice.destination.core.models.incoming_transferable import ( + IncomingTransferableState, +) + + +@pytest.mark.django_db() +def test__get_or_create_transferable_new_user_profile_new_transferable(): + a_transferable_range = protocol_factory.TransferableRangeFactory() + + assert models.UserProfile.objects.count() == 0 + assert models.IncomingTransferable.objects.count() == 0 + + transferable = transferable_range._get_or_create_transferable(a_transferable_range) + + assert models.UserProfile.objects.count() == 1 + + assert models.IncomingTransferable.objects.count() == 1 + assert transferable.size == a_transferable_range.transferable.size + assert transferable.sha1 is None + + +@pytest.mark.django_db() +def test__get_or_create_transferable_existing_user_profile_new_transferable(): + user_profile = factory.UserProfileFactory() + a_transferable_range = protocol_factory.TransferableRangeFactory( + transferable=protocol_factory.TransferableFactory( + user_profile_id=user_profile.associated_user_profile_id + ) + ) + + assert models.UserProfile.objects.count() == 1 + + transferable = transferable_range._get_or_create_transferable(a_transferable_range) + + assert models.UserProfile.objects.count() == 1 + assert transferable.user_profile == user_profile + + assert models.IncomingTransferable.objects.count() == 1 + assert transferable.size == a_transferable_range.transferable.size + assert transferable.sha1 is None + + +@pytest.mark.django_db() +def test__get_or_create_transferable_existing_user_profile_existing_transferable(): + user_profile = factory.UserProfileFactory() + expected_transferable = factory.IncomingTransferableFactory( + state=models.IncomingTransferableState.ONGOING, user_profile=user_profile + ) + + a_transferable_range = protocol_factory.TransferableRangeFactory( + transferable=protocol_factory.TransferableFactory( + id=expected_transferable.id, + user_profile_id=user_profile.associated_user_profile_id, + ), + ) + + assert models.UserProfile.objects.count() == 1 + assert models.IncomingTransferable.objects.count() == 1 + + transferable = transferable_range._get_or_create_transferable(a_transferable_range) + + assert models.UserProfile.objects.count() == 1 + assert models.IncomingTransferable.objects.count() == 1 + assert transferable.user_profile == user_profile + + assert transferable == expected_transferable + + +@pytest.mark.parametrize( + ("transferable_bytes_received", "byte_offset", "expected_exception"), + [ + (0, 1, transferable_range.MissedTransferableRangeError), + (0, 0, None), + (1, 1, None), + (1, 2, transferable_range.MissedTransferableRangeError), + ], +) +def test__assert_no_transferable_ranges_were_missed( + transferable_bytes_received: int, + byte_offset: int, + expected_exception: bool, + faker: Faker, +): + mocked_transferable = mock.create_autospec(models.IncomingTransferable) + mocked_transferable.bytes_received = transferable_bytes_received + + a_uuid = faker.uuid4() + # mock.create_autospec does not seem to work for class attributes + mocked_transferable_range = mock.Mock() + mocked_transferable_range.byte_offset = byte_offset + mocked_transferable_range.transferable.id = str(a_uuid) + + function_to_call = functools.partial( + transferable_range._assert_no_transferable_ranges_were_missed, + mocked_transferable_range, + mocked_transferable, + ) + + if expected_exception is None: + function_to_call() + else: + with pytest.raises(expected_exception): + function_to_call() + + +@pytest.mark.parametrize("state", set(IncomingTransferableState)) +@pytest.mark.django_db() +def test__transferable_is_ready(state: IncomingTransferableState): + if state.is_final: + with pytest.raises(transferable_range.TransferableAlreadyInFinalState): + transferable_range._assert_transferable_is_ready( + factory.IncomingTransferableFactory(state=state) + ) + else: + transferable_range._assert_transferable_is_ready( + factory.IncomingTransferableFactory(state=state) + ) + + +@pytest.mark.parametrize( + ( + "transferable_bytes_received", + "transferable_size", + "transferable_range_size", + "transferable_range_transferable_size", + "expected_exception", + "expect_warning", + ), + [ + # First TransferableRange, half the total Transferable + (0, 4, 2, 4, transferable_range.FinalSizeMismatchError, False), + # First TransferableRange containing all the data + (0, 2, 2, 2, None, False), + # First TransferableRange containing all the data, wrong size + (0, 1, 2, 2, None, True), + # First TransferableRange containing more than all the data + # (this should not be possible) + (0, None, 4, 2, transferable_range.FinalSizeMismatchError, False), + # TransferableRange containing all the data when it has already been received + (2, None, 2, 2, transferable_range.FinalSizeMismatchError, False), + # Last TransferableRange received while having already received the only + # other TransferableRange + (2, None, 2, 4, None, False), + # Last TransferableRange received while having already received the only + # other TransferableRange, wrong size + (2, 1, 2, 4, None, True), + ], +) +def test__assert_transferable_size_is_consistent( + transferable_bytes_received: int, + transferable_size: int, + transferable_range_size: int, + transferable_range_transferable_size: int, + expected_exception: Optional[transferable_range.FinalSizeMismatchError], + expect_warning: bool, + faker: Faker, + caplog: pytest.LogCaptureFixture, +): + caplog.set_level(logging.WARNING) + + mocked_transferable = mock.create_autospec(models.IncomingTransferable) + mocked_transferable.bytes_received = transferable_bytes_received + mocked_transferable.size = transferable_size + + a_uuid = faker.uuid4() + # mock.create_autospec does not seem to work for class attributes + mocked_transferable_range = mock.Mock() + mocked_transferable_range.data = b"0" * transferable_range_size + mocked_transferable_range.transferable.size = transferable_range_transferable_size + mocked_transferable_range.transferable.id = str(a_uuid) + + function_to_call = functools.partial( + transferable_range._assert_transferable_size_is_consistent, + mocked_transferable_range, + mocked_transferable, + ) + + if expected_exception is None: + function_to_call() + else: + with pytest.raises(expected_exception): + function_to_call() + + if expect_warning: + assert f"was initially announced with size {transferable_size}" in caplog.text + + +@pytest.mark.parametrize( + ( + "expected_computed_sha1", + "transferable_sha1_string", + "expected_exception", + ), + [ + # correct hash + ( + hashlib.sha1(b"I like waffles"), + bytes.fromhex("b761f36a6be4e40ac70ccc5c93b21775d1a99fc3"), + None, + ), + # incorrect hash + ( + hashlib.sha1(b"Small deeds done are better than great deeds planned."), + bytes.fromhex("b761f36a6be4e40ac70ccc5c93b21775d1a99fc3"), + transferable_range.FinalDigestMismatchError, + ), + ], +) +def test__assert_transferable_sha1_is_consistent( + expected_computed_sha1: "hashlib._Hash", + transferable_sha1_string: str, + expected_exception: Optional[transferable_range.FinalDigestMismatchError], + faker: Faker, +): + a_uuid = faker.uuid4() + # mock.create_autospec does not seem to work for class attributes + mocked_transferable_range = mock.Mock() + mocked_transferable_range.transferable.id = str(a_uuid) + mocked_transferable_range.transferable.sha1 = transferable_sha1_string + + function_to_call = functools.partial( + transferable_range._assert_transferable_sha1_is_consistent, + mocked_transferable_range, + expected_computed_sha1, + ) + + if expected_exception is None: + function_to_call() + else: + with pytest.raises(expected_exception): + function_to_call() + + +@pytest.mark.django_db() +def test__extract_data(): + data = b"hello " + sha1 = hashlib.sha1() + sha1.update(data) + sha1_intermediary = rehash.sha1_to_bytes(sha1) + mocked_transferable_range = mock.Mock() + mocked_transferable_range.data = b"world!" + + transferable = factory.IncomingTransferableFactory( + rehash_intermediary=sha1_intermediary, + state=models.IncomingTransferableState.ONGOING, + ) + data, new_sha1 = transferable_range._extract_data( + mocked_transferable_range, transferable + ) + + sha1.update(mocked_transferable_range.data) + assert sha1.digest() == new_sha1.digest() diff --git a/backend/tests/destination/unit/receiver/packet_handler/test_packet_handler.py b/backend/tests/destination/unit/receiver/packet_handler/test_packet_handler.py new file mode 100644 index 0000000..bdbd712 --- /dev/null +++ b/backend/tests/destination/unit/receiver/packet_handler/test_packet_handler.py @@ -0,0 +1,32 @@ +from unittest import mock + +from eurydice.common import protocol +from eurydice.destination.receiver.packet_handler import packet_handler + + +class TestOnTheWirePacketHandler: + @mock.patch( + "eurydice.destination.receiver.packet_handler.packet_handler.extractors", + autospec=True, + ) + def test_constructor_success(self, mocked_extractors: mock.Mock): + handler = packet_handler.OnTheWirePacketHandler() + mocked_extractors.TransferableRangeExtractor.assert_called_once() + mocked_extractors.TransferableRevocationExtractor.assert_called_once() + mocked_extractors.OngoingHistoryExtractor.assert_called_once() + + assert len(handler._extractors) == 3 + + @mock.patch( + "eurydice.destination.receiver.packet_handler.packet_handler.extractors", + autospec=True, + ) + def test_handle_success(self, mocked_extractors: mock.Mock): + handler = packet_handler.OnTheWirePacketHandler() + packet = mock.Mock(autospec=protocol.OnTheWirePacket) + handler.handle(packet) + + assert len(handler._extractors) == 3 + + for extractor in handler._extractors: + extractor.extract.assert_called_once_with(packet) diff --git a/backend/tests/destination/unit/receiver/packet_handler/test_s3_helpers.py b/backend/tests/destination/unit/receiver/packet_handler/test_s3_helpers.py new file mode 100644 index 0000000..20e7aed --- /dev/null +++ b/backend/tests/destination/unit/receiver/packet_handler/test_s3_helpers.py @@ -0,0 +1,29 @@ +import pytest +from faker import Faker + +from eurydice.common import minio +from eurydice.destination.receiver.packet_handler import s3_helpers +from tests.destination.integration import factory + + +@pytest.fixture() +def s3_bucket(faker: Faker): + bucket_name = faker.slug() + minio.client.make_bucket(bucket_name=bucket_name) + yield bucket_name + minio.client.remove_bucket(bucket_name=bucket_name) + + +@pytest.mark.django_db() +def test__initiate_multipart_upload(s3_bucket: str): + incoming_transferable = factory.IncomingTransferableFactory( + s3_bucket_name=s3_bucket + ) + upload_id = s3_helpers.initiate_multipart_upload(incoming_transferable) + list_parts_result = minio.client._list_parts( + bucket_name=s3_bucket, + object_name=str(incoming_transferable.id), + upload_id=upload_id, + ) + assert list_parts_result.object_name == str(incoming_transferable.id) + assert list_parts_result.bucket_name == s3_bucket diff --git a/backend/tests/destination/unit/receiver/test_main.py b/backend/tests/destination/unit/receiver/test_main.py new file mode 100644 index 0000000..2a2af4c --- /dev/null +++ b/backend/tests/destination/unit/receiver/test_main.py @@ -0,0 +1,182 @@ +import datetime +import logging +from unittest import mock + +import freezegun +import pytest +from django.conf import Settings +from django.utils import timezone + +from eurydice.common import protocol +from eurydice.common.utils import signals +from eurydice.destination.receiver import main +from eurydice.destination.receiver import packet_handler +from eurydice.destination.receiver import packet_receiver + + +@pytest.fixture() +def packet() -> mock.Mock: + return mock.Mock(autospec=protocol.OnTheWirePacket) + + +class TestPacketLogger: + @pytest.mark.parametrize( + ("is_empty", "expected_msg"), + [(True, "Heartbeat received"), (False, "{} received")], + ) + def test_log_received( + self, + is_empty: bool, + expected_msg: str, + packet: mock.Mock, + caplog: pytest.LogCaptureFixture, + ): + caplog.set_level(logging.INFO) + packet.is_empty.return_value = is_empty + + packet_logger = main._PacketLogger() + packet_logger.log_received(packet) + assert [expected_msg.format(packet)] == caplog.messages + + @freezegun.freeze_time() + @pytest.mark.parametrize("should_log", [True, False]) + def test_log_not_received( + self, should_log: bool, settings: Settings, caplog: pytest.LogCaptureFixture + ): + caplog.set_level(logging.ERROR) + settings.EXPECT_PACKET_EVERY = datetime.timedelta(seconds=1) + + packet_logger = main._PacketLogger() + + if should_log: + packet_logger._last_log_at = timezone.now() - settings.EXPECT_PACKET_EVERY + + packet_logger.log_not_received() + + if should_log: + assert [ + "No data packet or heartbeat received in the last 1 second. " + "Check the health of the sender on the origin side." + ] == caplog.messages + else: + assert not caplog.messages + + +@mock.patch.object(main._PacketLogger, "log_not_received") +@mock.patch.object(main._PacketLogger, "log_received") +@mock.patch.object(packet_handler.OnTheWirePacketHandler, "handle") +@mock.patch.object(signals.BooleanCondition, "__bool__") +@mock.patch("eurydice.destination.receiver.main.packet_receiver.PacketReceiver") +@pytest.mark.django_db() +def test_loop_receive_packet( + PacketReceiver: mock.Mock, # noqa: N803 + boolean_cond: mock.Mock, + handler: mock.Mock, + log_received: mock.Mock, + log_not_received: mock.Mock, + packet: mock.Mock, +): + receiver = mock.MagicMock(autospec=packet_receiver.PacketReceiver) + receiver.__enter__.return_value = receiver + receiver.receive.side_effect = [packet, packet_receiver.NothingToReceive] + PacketReceiver.return_value = receiver + + boolean_cond.return_value = False + main._loop() + + handler.assert_called_once_with(packet) + log_received.assert_called_once_with(packet) + log_not_received.assert_not_called() + + +@mock.patch.object(main._PacketLogger, "log_not_received") +@mock.patch.object(main._PacketLogger, "log_received") +@mock.patch.object(packet_handler.OnTheWirePacketHandler, "handle") +@mock.patch.object(signals.BooleanCondition, "__bool__") +@mock.patch("eurydice.destination.receiver.main.packet_receiver.PacketReceiver") +def test_loop_reception_error( + PacketReceiver: mock.Mock, # noqa: N803 + boolean_cond: mock.Mock, + handler: mock.Mock, + log_received: mock.Mock, + log_not_received: mock.Mock, + caplog: pytest.LogCaptureFixture, +): + receiver = mock.MagicMock(autospec=packet_receiver.PacketReceiver) + receiver.__enter__.return_value = receiver + receiver.receive.side_effect = [ + packet_receiver.ReceptionError, + packet_receiver.NothingToReceive, + ] + PacketReceiver.return_value = receiver + + boolean_cond.return_value = False + main._loop() + + handler.assert_not_called() + log_received.assert_not_called() + log_not_received.assert_not_called() + + assert "Error on packet reception." in caplog.text + + +@mock.patch.object(main._PacketLogger, "log_not_received") +@mock.patch.object(main._PacketLogger, "log_received") +@mock.patch.object(packet_handler.OnTheWirePacketHandler, "handle") +@mock.patch.object(signals.BooleanCondition, "__bool__") +@mock.patch("eurydice.destination.receiver.main.packet_receiver.PacketReceiver") +def test_loop_reception_timeout( + PacketReceiver: mock.Mock, # noqa: N803 + boolean_cond: mock.Mock, + handler: mock.Mock, + log_received: mock.Mock, + log_not_received: mock.Mock, +): + receiver = mock.MagicMock(autospec=packet_receiver.PacketReceiver) + receiver.__enter__.return_value = receiver + receiver.receive.side_effect = [packet_receiver.NothingToReceive] * 2 + PacketReceiver.return_value = receiver + + boolean_cond.side_effect = [True, False] + main._loop() + + handler.assert_not_called() + log_received.assert_not_called() + log_not_received.assert_called_once() + + +@mock.patch.object(main._PacketLogger, "log_not_received") +@mock.patch.object(main._PacketLogger, "log_received") +@mock.patch.object(packet_handler.OnTheWirePacketHandler, "handle") +@mock.patch.object(signals.BooleanCondition, "__bool__") +@mock.patch("eurydice.destination.receiver.main.packet_receiver.PacketReceiver") +def test_loop_unexpected_exception( + PacketReceiver: mock.Mock, # noqa: N803 + boolean_cond: mock.Mock, + handler: mock.Mock, + log_received: mock.Mock, + log_not_received: mock.Mock, + caplog: pytest.LogCaptureFixture, +): + caplog.set_level(logging.ERROR) + + receiver = mock.MagicMock(autospec=packet_receiver.PacketReceiver) + receiver.__enter__.return_value = receiver + receiver.receive.side_effect = [ + Exception("this is terrible"), + packet_receiver.NothingToReceive, + ] + PacketReceiver.return_value = receiver + + boolean_cond.side_effect = [False] + main._loop() + + handler.assert_not_called() + log_received.assert_not_called() + log_not_received.assert_not_called() + + assert "this is terrible" in caplog.text + assert ( + "An unexpected error occurred while processing an OnTheWirePacket" + in caplog.text + ) diff --git a/backend/tests/destination/unit/receiver/test_packet_receiver.py b/backend/tests/destination/unit/receiver/test_packet_receiver.py new file mode 100644 index 0000000..30fbd7b --- /dev/null +++ b/backend/tests/destination/unit/receiver/test_packet_receiver.py @@ -0,0 +1,38 @@ +import queue +import socket +from unittest import mock + +import pytest +from django.conf import settings + +from eurydice.destination.receiver import packet_receiver + + +class Test_Server: # noqa: N801 + @mock.patch.object(packet_receiver._RequestHandler, "handle") + def test_handle_error_is_logged( + self, mocked_handle_func: mock.Mock, caplog: pytest.LogCaptureFixture + ): + exc_msg = "Something terrible happened" + mocked_handle_func.side_effect = Exception(exc_msg) + + with packet_receiver._Server(receiving_queue=mock.Mock()) as server: + with socket.create_connection( + ("127.0.0.1", settings.PACKET_RECEIVER_PORT) + ) as conn: + conn.sendall(b"hello, world") + + server.handle_request() + + assert exc_msg in caplog.text + + +class Test_ReceiverThread: # noqa: N801 + def test_run_and_stop(self): + mocked_queue = mock.create_autospec(queue.Queue) + thread = packet_receiver._ReceiverThread(mocked_queue) + thread.start() + assert thread.is_alive() + thread.stop() + thread.join() + assert not thread.is_alive() diff --git a/backend/tests/destination/unit/receiver/test_transferable_ingestion.py b/backend/tests/destination/unit/receiver/test_transferable_ingestion.py new file mode 100644 index 0000000..f13a349 --- /dev/null +++ b/backend/tests/destination/unit/receiver/test_transferable_ingestion.py @@ -0,0 +1,58 @@ +import hashlib +from unittest import mock + +import pytest + +import tests.destination.integration.factory as factory +from eurydice.destination.core.models.incoming_transferable import ( + IncomingTransferableState, +) +from eurydice.destination.receiver import transferable_ingestion +from eurydice.destination.utils import rehash + + +@pytest.mark.django_db() +def test__get_next_part_number_parts_not_exist(): + transferable = factory.IncomingTransferableFactory() + + assert transferable_ingestion._get_next_part_number(transferable) == 1 + + +@pytest.mark.django_db() +def test__get_next_part_number_parts_exist(): + transferable = factory.IncomingTransferableFactory() + + factory.S3UploadPartFactory(part_number=1, incoming_transferable=transferable) + + assert transferable_ingestion._get_next_part_number(transferable) == 2 + + +@pytest.mark.django_db() +def test__update_incoming_transferable(): + data = b"hello " + sha1 = hashlib.sha1() + sha1.update(data) + sha1_intermediary = rehash.sha1_to_bytes(sha1) + mocked_transferable_range = mock.Mock() + mocked_transferable_range.data = b"world!" + + transferable = factory.IncomingTransferableFactory( + bytes_received=len(data), + size=None, + rehash_intermediary=sha1_intermediary, + state=IncomingTransferableState.ONGOING, + ) + + new_data = b"world" + sha1.update(new_data) + sha1_intermediary = rehash.sha1_to_bytes(sha1) + transferable_ingestion._update_incoming_transferable( + transferable, transferable_ingestion.PendingIngestionData(new_data, sha1, True) + ) + + assert transferable.sha1 == sha1.digest() + assert transferable.state == IncomingTransferableState.SUCCESS + assert transferable.finished_at is not None + assert transferable.bytes_received == len(data + new_data) + assert transferable.size == len(data + new_data) + assert transferable.rehash_intermediary == sha1_intermediary diff --git a/backend/tests/destination/unit/receiver/test_transferable_ingestion_fs.py b/backend/tests/destination/unit/receiver/test_transferable_ingestion_fs.py new file mode 100644 index 0000000..f35b815 --- /dev/null +++ b/backend/tests/destination/unit/receiver/test_transferable_ingestion_fs.py @@ -0,0 +1,81 @@ +import hashlib +from pathlib import Path +from unittest import mock + +import pytest +from django.conf import Settings + +import tests.destination.integration.factory as factory +from eurydice.destination.core.models.incoming_transferable import ( + IncomingTransferableState, +) +from eurydice.destination.receiver import transferable_ingestion_fs +from eurydice.destination.utils import rehash + + +@pytest.mark.django_db() +def test__abort_ingestion( + settings: Settings, + tmp_path: Path, +): + settings.TRANSFERABLE_STORAGE_DIR = tmp_path + transferable = factory.IncomingTransferableFactory( + state=IncomingTransferableState.ONGOING + ) + + transferable_ingestion_fs.abort_ingestion(transferable) + + assert transferable.state == IncomingTransferableState.ERROR + + +@pytest.mark.django_db() +def test__update_incoming_transferable( + settings: Settings, + tmp_path: Path, +): + settings.TRANSFERABLE_STORAGE_DIR = tmp_path + data = b"hello " + sha1 = hashlib.sha1() + sha1.update(data) + sha1_intermediary = rehash.sha1_to_bytes(sha1) + mocked_transferable_range = mock.Mock() + mocked_transferable_range.data = b"world!" + + transferable = factory.IncomingTransferableFactory( + bytes_received=len(data), + size=None, + rehash_intermediary=sha1_intermediary, + state=IncomingTransferableState.ONGOING, + ) + + new_data = b"world" + sha1.update(new_data) + sha1_intermediary = rehash.sha1_to_bytes(sha1) + + transferable_ingestion_fs.ingest( + transferable, + transferable_ingestion_fs.PendingIngestionData(new_data, sha1, False), + ) + + assert transferable.state == IncomingTransferableState.ONGOING + assert transferable.finished_at is None + assert transferable.bytes_received == len(data + new_data) + assert transferable.rehash_intermediary == sha1_intermediary + assert transferable_ingestion_fs._storage_exists(transferable) + + new_data2 = b"!" + sha1.update(new_data2) + sha1_intermediary = rehash.sha1_to_bytes(sha1) + + transferable_ingestion_fs.ingest( + transferable, + transferable_ingestion_fs.PendingIngestionData(new_data2, sha1, True), + ) + + assert transferable.sha1 == sha1.digest() + assert transferable.state == IncomingTransferableState.SUCCESS + assert transferable.finished_at is not None + assert transferable.bytes_received == len(data + new_data + new_data2) + assert transferable.size == len(data + new_data + new_data2) + assert transferable.rehash_intermediary == sha1_intermediary + assert transferable_ingestion_fs._storage_exists(transferable) diff --git a/backend/tests/destination/unit/storage/test_fs.py b/backend/tests/destination/unit/storage/test_fs.py new file mode 100644 index 0000000..ddb1f0c --- /dev/null +++ b/backend/tests/destination/unit/storage/test_fs.py @@ -0,0 +1,70 @@ +from pathlib import Path + +import pytest +from django.conf import Settings + +import eurydice.destination.storage.fs as fs +from tests.destination.integration import factory + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + ("storage_dir", "bucket_name", "object_name", "expected_path"), + [ + ( + "storage_dir", + "bucket_name", + "object_name", + Path("storage_dir/bucket_name/object_name"), + ), + ("1", "2", "3", Path("1/2/3")), + ("/", "2", "3", Path("/2/3")), + ], +) +def test_fs_file_path( + settings: Settings, + storage_dir: str, + bucket_name: str, + object_name: str, + expected_path: Path, +): + settings.TRANSFERABLE_STORAGE_DIR = storage_dir + + obj = factory.IncomingTransferableFactory( + s3_bucket_name=bucket_name, + s3_object_name=object_name, + ) + assert expected_path == fs.file_path(obj) + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + ("bucket_name", "object_name"), + [ + ( + "bucket_name", + "object_name", + ), + ("2", "3"), + ], +) +def test_fs_delete( + settings: Settings, + tmp_path: Path, + bucket_name: str, + object_name: str, +): + settings.TRANSFERABLE_STORAGE_DIR = tmp_path + + obj = factory.IncomingTransferableFactory( + s3_bucket_name=bucket_name, + s3_object_name=object_name, + ) + + file_path = fs.file_path(obj) + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.touch() + + fs.delete(obj) + + assert not file_path.exists() diff --git a/backend/tests/destination/unit/utils/__init__.py b/backend/tests/destination/unit/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/destination/unit/utils/test_rehash.py b/backend/tests/destination/unit/utils/test_rehash.py new file mode 100644 index 0000000..ab5fd03 --- /dev/null +++ b/backend/tests/destination/unit/utils/test_rehash.py @@ -0,0 +1,45 @@ +import hashlib + +import humanfriendly as hf +import hypothesis +from hypothesis import strategies + +from eurydice.destination.utils import rehash + + +@hypothesis.given(strategies.binary(max_size=hf.parse_size("10 MB"))) +@hypothesis.example(b"") +def test_sha1_to_bytes_success(data: bytes): + sha1 = hashlib.sha1() + sha1.update(data) + assert isinstance(rehash.sha1_to_bytes(sha1), bytes) + + +@hypothesis.given(strategies.binary(max_size=hf.parse_size("10 MB"))) +@hypothesis.example(b"") +def test_sha1_to_bytes_from_bytes_success(data: bytes): + sha1 = hashlib.sha1() + sha1.update(data) + + dump = rehash.sha1_to_bytes(sha1) + assert rehash.sha1_from_bytes(dump).digest() == sha1.digest() + + +@hypothesis.given( + strategies.binary(max_size=hf.parse_size("1 MB")), + strategies.integers(max_value=50), +) +def test_sha1_to_bytes_from_bytes_success_multiple_steps_success( + data: bytes, steps: int +): + sha1 = hashlib.sha1() + dump = rehash.sha1_to_bytes(sha1) + + for _ in range(steps): + resumed_sha1 = rehash.sha1_from_bytes(dump) + + resumed_sha1.update(data) + sha1.update(data) + + assert resumed_sha1.digest() == sha1.digest() + dump = rehash.sha1_to_bytes(resumed_sha1) diff --git a/backend/tests/origin/__init__.py b/backend/tests/origin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/integration/__init__.py b/backend/tests/origin/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/integration/api/__init__.py b/backend/tests/origin/integration/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/integration/api/test_http_error_format.py b/backend/tests/origin/integration/api/test_http_error_format.py new file mode 100644 index 0000000..c9484c0 --- /dev/null +++ b/backend/tests/origin/integration/api/test_http_error_format.py @@ -0,0 +1,33 @@ +from unittest import mock + +import django.urls +import pytest +from rest_framework import exceptions +from rest_framework import test + +from eurydice.origin.api.views.user_association import UserAssociationView +from tests.origin.integration import factory + + +@pytest.mark.django_db() +@pytest.mark.parametrize("exception", [exceptions.APIException, exceptions.ParseError]) +def test_association_token_error_is_json( + exception: exceptions.APIException, api_client: test.APIClient +): + """Tests that APIExceptions for HTTP errors 500 and 400 correctly return + a JSON formatted error. + + NOTE: we test this only for this specific endpoint as it should behave the + same regardless of which endpoint returns an HTTP error 500 and 400 + """ + user_profile = factory.UserProfileFactory() + + api_client.force_login(user=user_profile.user) + + url = django.urls.reverse("user-association") + + with mock.patch.object(UserAssociationView, "get", side_effect=exception): + response = api_client.get(url) + + assert response.status_code == exception.status_code + assert response.headers["Content-Type"] == "application/json" diff --git a/backend/tests/origin/integration/api/test_permissions.py b/backend/tests/origin/integration/api/test_permissions.py new file mode 100644 index 0000000..6d0dc7f --- /dev/null +++ b/backend/tests/origin/integration/api/test_permissions.py @@ -0,0 +1,30 @@ +from unittest import mock + +import pytest +from django import http +from faker import Faker + +from eurydice.common.api import permissions +from tests.origin.integration import factory + + +@pytest.mark.django_db() +class TestIsTransferableOwner: + def test_has_object_permission_authorized(self): + obj = factory.OutgoingTransferableFactory() + request = mock.Mock() + request.user.id = obj.user_profile.user.id + + assert permissions.IsTransferableOwner.has_object_permission( + self=None, request=request, view=None, obj=obj + ) + + def test_has_object_permission_unauthorized(self, faker: Faker): + obj = factory.OutgoingTransferableFactory() + request = mock.Mock() + request.user.id = faker.uuid4(cast_to=None) + + with pytest.raises(http.Http404): + permissions.IsTransferableOwner.has_object_permission( + self=None, request=request, view=None, obj=obj + ) diff --git a/backend/tests/origin/integration/api/test_username_http_header.py b/backend/tests/origin/integration/api/test_username_http_header.py new file mode 100644 index 0000000..9d38223 --- /dev/null +++ b/backend/tests/origin/integration/api/test_username_http_header.py @@ -0,0 +1,44 @@ +import django.urls +import pytest +from rest_framework import status +from rest_framework import test + +from tests.origin.integration import factory +from tests.utils import fake + + +@pytest.mark.django_db() +def test_http_header_with_authenticated_username_is_present_on_success( + api_client: test.APIClient, +): + user = factory.UserProfileFactory.create().user + url = django.urls.reverse("transferable-list") + api_client.force_login(user=user) + + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + assert response["Authenticated-User"] == user.username + + +class TestHttpHeaderOnFailure(test.APITransactionTestCase): + def test_http_header_with_authenticated_username_is_present_on_failure(self): + user = factory.UserProfileFactory.create().user + url = django.urls.reverse("transferable-detail", kwargs={"pk": fake.uuid4()}) + self.client.force_login(user=user) + + response = self.client.get(url) + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response["Authenticated-User"] == user.username + + +@pytest.mark.django_db() +def test_http_header_with_authenticated_username_is_not_present_when_not_authenticated( + api_client: test.APIClient, +): + url = django.urls.reverse("transferable-list") + response = api_client.get(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert not response.has_header("Authenticated-User") diff --git a/backend/tests/origin/integration/cleaning/__init__.py b/backend/tests/origin/integration/cleaning/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/integration/cleaning/dbtrimmer/__init__.py b/backend/tests/origin/integration/cleaning/dbtrimmer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/integration/cleaning/dbtrimmer/test_dbtrimmer.py b/backend/tests/origin/integration/cleaning/dbtrimmer/test_dbtrimmer.py new file mode 100644 index 0000000..031da98 --- /dev/null +++ b/backend/tests/origin/integration/cleaning/dbtrimmer/test_dbtrimmer.py @@ -0,0 +1,38 @@ +import os +import subprocess +import sys + +import pytest +from django.conf import settings + +from eurydice.origin.cleaning import dbtrimmer + + +@pytest.mark.django_db() +def test_start_and_graceful_shutdown(): + with subprocess.Popen( + [sys.executable, "-m", dbtrimmer.__name__], + cwd=os.path.dirname(settings.BASE_DIR), + stderr=subprocess.PIPE, + env={ + "DB_NAME": settings.DATABASES["default"]["NAME"], + "DB_USER": settings.DATABASES["default"]["USER"], + "DB_PASSWORD": settings.DATABASES["default"]["PASSWORD"], + "DB_HOST": settings.DATABASES["default"]["HOST"], + "DB_PORT": str(settings.DATABASES["default"]["PORT"]), + "MINIO_ENDPOINT": settings.MINIO_ENDPOINT, + "MINIO_ACCESS_KEY": settings.MINIO_ACCESS_KEY, + "MINIO_SECRET_KEY": settings.MINIO_SECRET_KEY, + "MINIO_BUCKET_NAME": settings.MINIO_BUCKET_NAME, + "TRANSFERABLE_STORAGE_DIR": settings.TRANSFERABLE_STORAGE_DIR, + "LIDIS_HOST": settings.LIDIS_HOST, + "LIDIS_PORT": str(settings.LIDIS_PORT), + "USER_ASSOCIATION_TOKEN_SECRET_KEY": settings.USER_ASSOCIATION_TOKEN_SECRET_KEY, # noqa: E501 + }, + ) as proc: + while b"Ready" not in proc.stderr.readline(): + pass + + proc.terminate() + return_code = proc.wait() + assert return_code == 0 diff --git a/backend/tests/origin/integration/cleaning/dbtrimmer/test_main.py b/backend/tests/origin/integration/cleaning/dbtrimmer/test_main.py new file mode 100644 index 0000000..a6988d6 --- /dev/null +++ b/backend/tests/origin/integration/cleaning/dbtrimmer/test_main.py @@ -0,0 +1,266 @@ +import datetime +from typing import List +from unittest import mock + +import freezegun +import pytest +from django import conf +from django.utils import timezone +from faker import Faker + +import eurydice.origin.cleaning.dbtrimmer.dbtrimmer as dbtrimmer_module +from eurydice.common.enums import OutgoingTransferableState +from eurydice.common.utils import signals +from eurydice.origin.cleaning.dbtrimmer.dbtrimmer import OriginDBTrimmer +from eurydice.origin.core import models +from tests.origin.integration import factory + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + ( + "transferable_states", + "expected_deleted_transferables", + "expected_deleted_ranges", + "expected_deleted_revocations", + ), + [ + ([], 0, 0, 0), + ([OutgoingTransferableState.PENDING], 0, 0, 0), + ([OutgoingTransferableState.ONGOING], 0, 0, 0), + ([OutgoingTransferableState.ERROR], 1, 0, 1), + ([OutgoingTransferableState.CANCELED], 1, 0, 1), + ([OutgoingTransferableState.SUCCESS], 1, 1, 0), + ( + [ + OutgoingTransferableState.SUCCESS, + OutgoingTransferableState.SUCCESS, + ], + 2, + 2, + 0, + ), + ( + [ + OutgoingTransferableState.PENDING, + OutgoingTransferableState.ONGOING, + OutgoingTransferableState.ERROR, + OutgoingTransferableState.CANCELED, + OutgoingTransferableState.SUCCESS, + ], + 3, + 1, + 2, + ), + ], +) +def test_dbtrimmer_by_transferable_states( + faker: Faker, + transferable_states: List[OutgoingTransferableState], + expected_deleted_transferables: int, + expected_deleted_ranges: int, + expected_deleted_revocations: int, + caplog: pytest.LogCaptureFixture, + settings: conf.Settings, +): + settings.DBTRIMMER_TRIM_TRANSFERABLES_AFTER = datetime.timedelta(seconds=1) + + now = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + + for state in transferable_states: + updated_at = ( + now + - settings.DBTRIMMER_TRIM_TRANSFERABLES_AFTER + - datetime.timedelta(seconds=1) + ) + + with freezegun.freeze_time(updated_at): + factory.OutgoingTransferableFactory( + _submission_succeeded=(state == OutgoingTransferableState.SUCCESS), + _submission_succeeded_at=updated_at, + make_transferable_ranges_for_state=state, + ) + + with freezegun.freeze_time(now): + OriginDBTrimmer()._run() + + if ( + expected_deleted_ranges == 0 + and expected_deleted_revocations == 0 + and expected_deleted_transferables == 0 + ): + assert caplog.messages == [ + "DBTrimmer is running", + "DBTrimmer finished running", + ] + else: + assert caplog.messages == [ + "DBTrimmer is running", + f"DBTrimmer will remove {expected_deleted_transferables} " + "OutgoingTransferables " + f"and all associated objects.", + f"DBTrimmer successfully removed {expected_deleted_transferables} " + f"transferables, {expected_deleted_ranges} ranges, and " + f"{expected_deleted_revocations} revocations.", + "DBTrimmer finished running", + ] + + assert not models.OutgoingTransferable.objects.filter( + state__in=( + OutgoingTransferableState.SUCCESS, + OutgoingTransferableState.ERROR, + OutgoingTransferableState.CANCELED, + ) + ).exists() + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + ("time_delta", "expected_deletions"), + [ + ([], 0), + ([datetime.timedelta(seconds=1)], 0), + ([datetime.timedelta(seconds=59)], 0), + ([datetime.timedelta(seconds=61)], 1), + ( + [ + datetime.timedelta(seconds=59), + datetime.timedelta(seconds=61), + ], + 1, + ), + ( + [ + datetime.timedelta(seconds=69), + datetime.timedelta(seconds=71), + ], + 2, + ), + ], +) +def test_dbtrimmer_by_transferable_finish_date( + faker: Faker, + time_delta: List[datetime.timedelta], + expected_deletions: int, + caplog: pytest.LogCaptureFixture, + settings: conf.Settings, +): + settings.DBTRIMMER_TRIM_TRANSFERABLES_AFTER = datetime.timedelta(seconds=60) + + now = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + + for delta in time_delta: + submission_succeeded_at = now - delta + with freezegun.freeze_time(submission_succeeded_at): + factory.OutgoingTransferableFactory( + _submission_succeeded=False, + _submission_succeeded_at=submission_succeeded_at, + make_transferable_ranges_for_state=OutgoingTransferableState.CANCELED, + ) + + with freezegun.freeze_time(now): + OriginDBTrimmer()._run() + + if expected_deletions == 0: + assert caplog.messages == [ + "DBTrimmer is running", + "DBTrimmer finished running", + ] + else: + assert caplog.messages == [ + "DBTrimmer is running", + f"DBTrimmer will remove {expected_deletions} OutgoingTransferables " + f"and all associated objects.", + f"DBTrimmer successfully removed {expected_deletions} transferables, " + f"0 ranges, and {expected_deletions} revocations.", + "DBTrimmer finished running", + ] + + +@pytest.mark.django_db() +@mock.patch.object(signals.BooleanCondition, "__bool__") +@mock.patch("time.sleep") +def test_loop_success_no_transferable( + time_sleep: mock.Mock, + boolean_cond: mock.Mock, +): + boolean_cond.side_effect = [True, True, False] + dbtrimmer = OriginDBTrimmer() + dbtrimmer._loop() + assert time_sleep.call_count == 2 + + +@mock.patch("eurydice.origin.core.models.OutgoingTransferable.objects.filter") +@pytest.mark.django_db() +def test_deletion_count_mismatch_is_logged( + filter_mock: mock.Mock, + caplog: pytest.LogCaptureFixture, +): + sliced_qs = mock.Mock() + sliced_qs.values_list.return_value = ["xxx"] + qs = mock.MagicMock() + qs.__getitem__.return_value = sliced_qs + filter_mock.return_value = qs + filter_mock().delete.return_value = (None, {"": 0}) + + OriginDBTrimmer()._run() + + assert caplog.messages == [ + "DBTrimmer is running", + "DBTrimmer will remove 1 OutgoingTransferables and all associated objects.", + "DBTrimmer successfully removed 0 transferables, 0 ranges, and 0 revocations.", + "DBTrimmer deleted 0 OutgoingTransferables, instead of the expected 1.", + "DBTrimmer finished running", + ] + + +@pytest.mark.django_db() +def test_dbtrimmer_bulk_delete( + faker: Faker, + caplog: pytest.LogCaptureFixture, + settings: conf.Settings, +): + old_value = dbtrimmer_module.BULK_DELETION_SIZE + dbtrimmer_module.BULK_DELETION_SIZE = 1 + settings.DBTRIMMER_TRIM_TRANSFERABLES_AFTER = datetime.timedelta(seconds=1) + + now = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + + for state in [ + OutgoingTransferableState.SUCCESS, + OutgoingTransferableState.SUCCESS, + ]: + updated_at = ( + now + - settings.DBTRIMMER_TRIM_TRANSFERABLES_AFTER + - datetime.timedelta(seconds=1) + ) + + with freezegun.freeze_time(updated_at): + factory.OutgoingTransferableFactory( + _submission_succeeded=(state == OutgoingTransferableState.SUCCESS), + _submission_succeeded_at=updated_at, + make_transferable_ranges_for_state=state, + ) + + with freezegun.freeze_time(now): + OriginDBTrimmer()._run() + + assert caplog.messages == [ + "DBTrimmer is running", + "DBTrimmer will remove 1 OutgoingTransferables " "and all associated objects.", + "DBTrimmer successfully removed 1 transferables, 1 ranges, and 0 revocations.", + "DBTrimmer will remove 1 OutgoingTransferables " "and all associated objects.", + "DBTrimmer successfully removed 1 transferables, 1 ranges, and 0 revocations.", + "DBTrimmer finished running", + ] + + assert not models.OutgoingTransferable.objects.filter( + state__in=( + OutgoingTransferableState.SUCCESS, + OutgoingTransferableState.ERROR, + OutgoingTransferableState.CANCELED, + ) + ).exists() + + dbtrimmer_module.BULK_DELETION_SIZE = old_value diff --git a/backend/tests/origin/integration/core/__init__.py b/backend/tests/origin/integration/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/integration/core/management/__init__.py b/backend/tests/origin/integration/core/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/integration/core/management/commands/__init__.py b/backend/tests/origin/integration/core/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/integration/core/management/commands/test_populate_db.py b/backend/tests/origin/integration/core/management/commands/test_populate_db.py new file mode 100644 index 0000000..a92e5f0 --- /dev/null +++ b/backend/tests/origin/integration/core/management/commands/test_populate_db.py @@ -0,0 +1,17 @@ +import pytest +from django.contrib import auth +from django.core.management import call_command + +from eurydice.origin.core import models + + +@pytest.mark.django_db() +def test_populate_db(): + args = [] + opts = {"users": 1, "outgoing_transferables": 2, "transferable_ranges": 3} + call_command("populate_db", *args, **opts) + + assert models.UserProfile.objects.count() == opts["users"] + assert auth.get_user_model().objects.count() == opts["users"] + assert models.OutgoingTransferable.objects.count() == opts["outgoing_transferables"] + assert models.TransferableRange.objects.count() == opts["transferable_ranges"] diff --git a/backend/tests/origin/integration/core/models/__init__.py b/backend/tests/origin/integration/core/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/integration/core/models/test_outgoing_transferable.py b/backend/tests/origin/integration/core/models/test_outgoing_transferable.py new file mode 100644 index 0000000..ee952da --- /dev/null +++ b/backend/tests/origin/integration/core/models/test_outgoing_transferable.py @@ -0,0 +1,898 @@ +import datetime +import hashlib +from typing import Any +from typing import Dict +from typing import List +from typing import Optional + +import freezegun +import pytest +from django.db.models.expressions import Value +from django.utils import timezone +from faker import Faker + +from eurydice.common import enums +from eurydice.origin.core import enums as origin_enums +from eurydice.origin.core import models +from eurydice.origin.core.models import ( + outgoing_transferable as outgoing_transferable_model, +) +from tests.origin.integration import factory + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + ( + "submission_succeeded", + "revocation_reason", + "transferable_ranges_states", + "expected_state", + ), + [ + # Test when Transferable has been canceled + ( + False, + enums.TransferableRevocationReason.USER_CANCELED, + [], + enums.OutgoingTransferableState.CANCELED, + ), + # Test when Transferable has been revoked for another reason + ( + False, + enums.TransferableRevocationReason.UPLOAD_SIZE_MISMATCH, + [], + enums.OutgoingTransferableState.ERROR, + ), + # Test when Transferable has been revoked for another reason + ( + False, + enums.TransferableRevocationReason.OBJECT_STORAGE_FULL, + [], + enums.OutgoingTransferableState.ERROR, + ), + # Test when Transferable has been revoked for another reason + ( + False, + enums.TransferableRevocationReason.UNEXPECTED_EXCEPTION, + [], + enums.OutgoingTransferableState.ERROR, + ), + # Test when Transferable submission in ongoing + # but one TransferableRange has failed + ( + False, + None, + [ + origin_enums.TransferableRangeTransferState.ERROR, + origin_enums.TransferableRangeTransferState.TRANSFERRED, + ], + enums.OutgoingTransferableState.ERROR, + ), + # test when submission is finished and all TransferableRanges + # have been transferred + ( + True, + None, + [ + origin_enums.TransferableRangeTransferState.TRANSFERRED, + origin_enums.TransferableRangeTransferState.TRANSFERRED, + ], + enums.OutgoingTransferableState.SUCCESS, + ), + # test when the submission is ongoing but not TransferableRange + # has yet been created + ( + False, + None, + [], + enums.OutgoingTransferableState.PENDING, + ), + # test when submission is ongoing but TransferableRanges have not + # yet been transferred + ( + False, + None, + [ + origin_enums.TransferableRangeTransferState.PENDING, + origin_enums.TransferableRangeTransferState.PENDING, + ], + enums.OutgoingTransferableState.PENDING, + ), + # test when submission is ongoing, one TransferableRange has been transferred + # and another is still pending + ( + False, + None, + [ + origin_enums.TransferableRangeTransferState.TRANSFERRED, + origin_enums.TransferableRangeTransferState.PENDING, + ], + enums.OutgoingTransferableState.ONGOING, + ), + # test when submission is done but some TransferableRanges have not + # yet been transferred + ( + True, + None, + [ + origin_enums.TransferableRangeTransferState.TRANSFERRED, + origin_enums.TransferableRangeTransferState.PENDING, + ], + enums.OutgoingTransferableState.ONGOING, + ), + ], +) +def test_outgoing_transferable_state( + submission_succeeded: bool, + revocation_reason: enums.TransferableRevocationReason, + transferable_ranges_states: origin_enums.TransferableRangeTransferState, + expected_state: enums.OutgoingTransferableState, + faker: Faker, +): + if submission_succeeded: + submission_succeeded_at = faker.date_time_this_decade( + tzinfo=timezone.get_current_timezone() + ) + else: + submission_succeeded_at = None + + outgoing_transferable = factory.OutgoingTransferableFactory( + _submission_succeeded=submission_succeeded, + _submission_succeeded_at=submission_succeeded_at, + ) + + if revocation_reason is not None: + factory.TransferableRevocationFactory( + reason=revocation_reason, outgoing_transferable=outgoing_transferable + ) + + for transferable_ranges_state in transferable_ranges_states: + factory.TransferableRangeFactory( + transfer_state=transferable_ranges_state, + outgoing_transferable=outgoing_transferable, + ) + + queried_outgoing_transferable = models.OutgoingTransferable.objects.get( + id=outgoing_transferable.id + ) + + assert queried_outgoing_transferable.state == expected_state + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + ( + "size_is_already_known", + "transferable_ranges_states", + "expected_progress", + ), + [ + # test when the submission is ongoing but no TransferableRange + # has yet been created + ( + False, + [], + None, + ), + # test when submission is ongoing but TransferableRanges have not + # yet been transferred + ( + False, + [ + origin_enums.TransferableRangeTransferState.PENDING, + origin_enums.TransferableRangeTransferState.PENDING, + ], + None, + ), + # test when submission is ongoing, one TransferableRange has been transferred + # and another is still pending + ( + False, + [ + origin_enums.TransferableRangeTransferState.TRANSFERRED, + origin_enums.TransferableRangeTransferState.PENDING, + ], + None, + ), + # test when submission is done but TransferableRanges have not + # yet been transferred + ( + True, + [ + origin_enums.TransferableRangeTransferState.PENDING, + origin_enums.TransferableRangeTransferState.PENDING, + ], + 0, + ), + # test when submission is done but some TransferableRanges have not + # yet been transferred + ( + True, + [ + origin_enums.TransferableRangeTransferState.TRANSFERRED, + origin_enums.TransferableRangeTransferState.PENDING, + ], + 50, + ), + # test when submission is done but some TransferableRanges have not + # yet been transferred + ( + True, + [ + origin_enums.TransferableRangeTransferState.TRANSFERRED, + origin_enums.TransferableRangeTransferState.TRANSFERRED, + origin_enums.TransferableRangeTransferState.PENDING, + ], + 66, + ), + # test when submission is finished and all TransferableRanges + # have been transferred + ( + True, + [ + origin_enums.TransferableRangeTransferState.TRANSFERRED, + origin_enums.TransferableRangeTransferState.TRANSFERRED, + ], + 100, + ), + # Test when Transferable submission is done + # but one TransferableRange has failed + ( + True, + [ + origin_enums.TransferableRangeTransferState.ERROR, + origin_enums.TransferableRangeTransferState.TRANSFERRED, + ], + 50, + ), + ], +) +def test_outgoing_transferable_progress( + size_is_already_known: bool, + transferable_ranges_states: origin_enums.TransferableRangeTransferState, + expected_progress: int, +): + transferable_range_length = 256 + + outgoing_transferable = factory.OutgoingTransferableFactory( + size=( + transferable_range_length * len(transferable_ranges_states) + if size_is_already_known + else None + ), + _submission_succeeded=False, + ) + + for transferable_ranges_state in transferable_ranges_states: + factory.TransferableRangeFactory( + transfer_state=transferable_ranges_state, + outgoing_transferable=outgoing_transferable, + size=transferable_range_length, + ) + + queried_outgoing_transferable = models.OutgoingTransferable.objects.get( + id=outgoing_transferable.id + ) + + assert queried_outgoing_transferable.progress == expected_progress + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + ( + "transferable_range_state", + "expected_progress", + ), + [ + (origin_enums.TransferableRangeTransferState.PENDING, 0), + (origin_enums.TransferableRangeTransferState.TRANSFERRED, 100), + ], +) +def test_outgoing_transferable_progress_empty_file( + transferable_range_state: origin_enums.TransferableRangeTransferState, + expected_progress: int, +): + outgoing_transferable = factory.OutgoingTransferableFactory(size=0) + + factory.TransferableRangeFactory( + transfer_state=transferable_range_state, + outgoing_transferable=outgoing_transferable, + size=0, + ) + + queried_outgoing_transferable = models.OutgoingTransferable.objects.get( + id=outgoing_transferable.id + ) + + assert queried_outgoing_transferable.progress == expected_progress + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + ( + "transferable_ranges_params", + "outgoing_transferable_params", + "expected_transfer_finished_at", + ), + [ + # test without any transferable ranges + ( + [], + { + "size": 1, + "submission_succeeded_at": None, + }, + None, + ), + # test with null sized Transferable + ( + [], + { + "size": 0, + "submission_succeeded_at": None, + }, + None, + ), + # test with null sized Transferable, sent + ( + [ + { + "byte_offset": 0, + "size": 0, + "transfer_state": ( + origin_enums.TransferableRangeTransferState.TRANSFERRED + ), + "finished_at": datetime.datetime( + 2021, 1, 1, tzinfo=timezone.get_current_timezone() + ), + } + ], + { + "size": 0, + "submission_succeeded_at": datetime.datetime( + 2021, 1, 1, tzinfo=timezone.get_current_timezone() + ), + }, + datetime.datetime(2021, 1, 1, tzinfo=timezone.get_current_timezone()), + ), + # test with one Transferable Range but not all of them + ( + [ + { + "byte_offset": 0, + "size": 1, + "transfer_state": ( + origin_enums.TransferableRangeTransferState.TRANSFERRED + ), + "finished_at": datetime.datetime( + 2021, 1, 1, tzinfo=timezone.get_current_timezone() + ), + } + ], + { + "size": 2, + "submission_succeeded_at": None, + }, + None, + ), + # test with one Transferable Range + ( + [ + { + "byte_offset": 0, + "size": 1, + "transfer_state": ( + origin_enums.TransferableRangeTransferState.TRANSFERRED + ), + "finished_at": datetime.datetime( + 2021, 1, 1, tzinfo=timezone.get_current_timezone() + ), + } + ], + { + "size": 1, + "submission_succeeded_at": datetime.datetime( + 2021, 1, 1, tzinfo=timezone.get_current_timezone() + ), + }, + datetime.datetime(2021, 1, 1, tzinfo=timezone.get_current_timezone()), + ), + # test with two Transferable Ranges + ( + [ + { + "byte_offset": 0, + "size": 1, + "transfer_state": ( + origin_enums.TransferableRangeTransferState.TRANSFERRED + ), + "finished_at": datetime.datetime( + 2021, 1, 1, tzinfo=timezone.get_current_timezone() + ), + }, + { + "byte_offset": 1, + "size": 1, + "transfer_state": ( + origin_enums.TransferableRangeTransferState.TRANSFERRED + ), + "finished_at": datetime.datetime( + 2021, 2, 1, tzinfo=timezone.get_current_timezone() + ), + }, + ], + { + "size": 2, + "submission_succeeded_at": datetime.datetime( + 2021, 1, 1, tzinfo=timezone.get_current_timezone() + ), + }, + datetime.datetime(2021, 2, 1, tzinfo=timezone.get_current_timezone()), + ), + # test with two Transferable Ranges, one not sent + ( + [ + { + "byte_offset": 0, + "size": 1, + "transfer_state": ( + origin_enums.TransferableRangeTransferState.TRANSFERRED + ), + "finished_at": datetime.datetime( + 2021, 2, 1, tzinfo=timezone.get_current_timezone() + ), + }, + { + "byte_offset": 1, + "size": 1, + "transfer_state": ( + origin_enums.TransferableRangeTransferState.PENDING + ), + "finished_at": None, + }, + ], + { + "size": 2, + "submission_succeeded_at": datetime.datetime( + 2021, 1, 1, tzinfo=timezone.get_current_timezone() + ), + }, + None, + ), + ], +) +def test_outgoing_transferable_range_transfer_finished_at( + transferable_ranges_params: List[Dict[str, Any]], + outgoing_transferable_params: Dict[str, Any], + expected_transfer_finished_at: Optional[datetime.datetime], +): + outgoing_transferable = factory.OutgoingTransferableFactory( + **outgoing_transferable_params + ) + + # create transferable ranges in byte offset order + for transferable_range_params in transferable_ranges_params: + transferable_range_params["outgoing_transferable"] = outgoing_transferable + with freezegun.freeze_time(transferable_range_params["finished_at"]): + factory.TransferableRangeFactory(**transferable_range_params) + + queried_outgoing_transferable = models.OutgoingTransferable.objects.get( + id=outgoing_transferable.id + ) + + assert ( + queried_outgoing_transferable.transfer_finished_at + == expected_transfer_finished_at + ) + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + ( + "transferable_ranges_states", + "expected_transferred_transferables_ranges_count", + ), + [ + # test with Transferred TransferableRanges + ( + [ + origin_enums.TransferableRangeTransferState.TRANSFERRED, + origin_enums.TransferableRangeTransferState.TRANSFERRED, + ], + 2, + ), + # test with one pending TransferableRange + ( + [ + origin_enums.TransferableRangeTransferState.TRANSFERRED, + origin_enums.TransferableRangeTransferState.PENDING, + ], + 1, + ), + # test without any TransferableRanges + ( + [], + 0, + ), + # Test with TransferableRanges in all but the TRANSFERRED state + ( + [ + origin_enums.TransferableRangeTransferState.PENDING, + origin_enums.TransferableRangeTransferState.CANCELED, + origin_enums.TransferableRangeTransferState.ERROR, + ], + 0, + ), + ], +) +def test_auto_transferred_ranges_count_trigger( + transferable_ranges_states: origin_enums.TransferableRangeTransferState, + expected_transferred_transferables_ranges_count: int, +): + outgoing_transferable = factory.OutgoingTransferableFactory() + + for transferable_ranges_state in transferable_ranges_states: + factory.TransferableRangeFactory( + transfer_state=transferable_ranges_state, + outgoing_transferable=outgoing_transferable, + ) + + queried_outgoing_transferable = models.OutgoingTransferable.objects.get( + id=outgoing_transferable.id + ) + + assert ( + queried_outgoing_transferable.auto_transferred_ranges_count + == expected_transferred_transferables_ranges_count + ) + + +@pytest.mark.django_db() +def test_auto_state_update_trigger(): + outgoing_transferable = factory.OutgoingTransferableFactory( + submission_succeeded_at=None, + bytes_received=0, + sha1=None, + size=None, + ) + + old_value = outgoing_transferable.auto_state_updated_at + + assert old_value is not None + + new_value = old_value + datetime.timedelta(minutes=1) + + with freezegun.freeze_time(new_value): + outgoing_transferable.sha1 = hashlib.sha1(b"0").digest() + outgoing_transferable.size = 1 + outgoing_transferable.bytes_received = 1 + outgoing_transferable.submission_succeeded_at = new_value + outgoing_transferable.save( + update_fields=[ + "submission_succeeded_at", + "bytes_received", + "size", + "sha1", + ] + ) + + outgoing_transferable.refresh_from_db() + assert outgoing_transferable.auto_state_updated_at == new_value + + +@pytest.mark.django_db() +def test_auto_state_update_trigger_not_altered_if_state_unchanged(faker: Faker): + outgoing_transferable = factory.OutgoingTransferableFactory( + submission_succeeded_at=faker.date_time_this_decade( + tzinfo=timezone.get_current_timezone() + ), + bytes_received=1, + sha1=hashlib.sha1(b"0").digest(), + size=1, + ) + + old_value = outgoing_transferable.auto_state_updated_at + + with freezegun.freeze_time( + outgoing_transferable.submission_succeeded_at + datetime.timedelta(hours=1) + ): + outgoing_transferable.name = "hey" + outgoing_transferable.save(update_fields=["name"]) + + outgoing_transferable.refresh_from_db() + assert outgoing_transferable.auto_state_updated_at == old_value + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + ( + "transferable_ranges_params", + "expected_bytes_transferred", + ), + [ + # test with Transferred TransferableRanges + ( + [ + { + "transfer_state": ( + origin_enums.TransferableRangeTransferState.TRANSFERRED + ), + "size": 1, + }, + { + "transfer_state": ( + origin_enums.TransferableRangeTransferState.TRANSFERRED + ), + "size": 1, + }, + ], + 2, + ), + # test with one pending TransferableRange + ( + [ + { + "transfer_state": ( + origin_enums.TransferableRangeTransferState.TRANSFERRED + ), + "size": 1, + }, + { + "transfer_state": ( + origin_enums.TransferableRangeTransferState.PENDING + ), + "size": 1, + }, + ], + 1, + ), + # test without any TransferableRanges + ( + [], + 0, + ), + # Test with TransferableRanges in all but the TRANSFERRED state + ( + [ + { + "transfer_state": ( + origin_enums.TransferableRangeTransferState.PENDING + ), + "size": 1, + }, + { + "transfer_state": ( + origin_enums.TransferableRangeTransferState.CANCELED + ), + "size": 1, + }, + { + "transfer_state": ( + origin_enums.TransferableRangeTransferState.ERROR + ), + "size": 1, + }, + ], + 0, + ), + ], +) +def test_bytes_transferred_annotation( + transferable_ranges_params: List[dict], + expected_bytes_transferred: int, +): + outgoing_transferable = factory.OutgoingTransferableFactory() + + for transferable_range_params in transferable_ranges_params: + transferable_range_params["outgoing_transferable"] = outgoing_transferable + factory.TransferableRangeFactory(**transferable_range_params) + + queried_outgoing_transferable = models.OutgoingTransferable.objects.get( + id=outgoing_transferable.id + ) + + assert ( + queried_outgoing_transferable.auto_bytes_transferred + == expected_bytes_transferred + ) + + +@pytest.mark.django_db() +def test__build_transfer_duration_annotation_with_transfer_finished_at(): + """ + Test computing an OutgoingTransferable's `transfer_duration` when + the transfer is finished + """ + created_at = datetime.datetime( + year=1998, + month=2, + day=4, + tzinfo=timezone.get_current_timezone(), + ) + transfer_finished_at = datetime.datetime( + year=1998, + month=2, + day=5, + tzinfo=timezone.get_current_timezone(), + ) + expected_transfer_duration = datetime.timedelta(days=1).total_seconds() + transferable_size = 2 + + with freezegun.freeze_time(created_at): + outgoing_transferable = factory.OutgoingTransferableFactory( + size=transferable_size, submission_succeeded_at=transfer_finished_at + ) + + factory.TransferableRangeFactory( + finished_at=transfer_finished_at, + outgoing_transferable=outgoing_transferable, + size=transferable_size, + transfer_state=origin_enums.TransferableRangeTransferState.TRANSFERRED, + ) + + queried_outgoing_transferable = models.OutgoingTransferable.objects.annotate( + transfer_duration=( + outgoing_transferable_model._build_transfer_duration_annotation() + ) + ).get(id=outgoing_transferable.id) + + assert queried_outgoing_transferable.transfer_duration == expected_transfer_duration + + +@pytest.mark.django_db() +def test__build_transfer_duration_annotation_no_transfer_finished_at( + faker: Faker, +): + """ + Test computing an OutgoingTransferable's `transfer_duration` when + the transfer is not yet finished + """ + created_at = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + transfer_finished_at = None + transferable_size = 2 + current_datetime = timezone.now() + + with freezegun.freeze_time(created_at): + outgoing_transferable = factory.OutgoingTransferableFactory( + size=transferable_size, submission_succeeded_at=transfer_finished_at + ) + + factory.TransferableRangeFactory( + finished_at=transfer_finished_at, + outgoing_transferable=outgoing_transferable, + size=transferable_size, + transfer_state=origin_enums.TransferableRangeTransferState.PENDING, + ) + + with freezegun.freeze_time(current_datetime): + queried_outgoing_transferable = models.OutgoingTransferable.objects.annotate( + transfer_duration=( + outgoing_transferable_model._build_transfer_duration_annotation() + ) + ).get(id=outgoing_transferable.id) + + assert queried_outgoing_transferable.transfer_duration == int( + round((current_datetime - created_at).total_seconds()) + ) + + +@pytest.mark.django_db() +def test__build_transfer_speed_annotation_with_non_zero_transfer_duration(): + created_at = datetime.datetime( + year=1998, + month=2, + day=4, + tzinfo=timezone.get_current_timezone(), + ) + transfer_finished_at = created_at + datetime.timedelta(seconds=1) + transferable_size = 2 + expected_transfer_speed = 2 + + with freezegun.freeze_time(created_at): + outgoing_transferable = factory.OutgoingTransferableFactory( + size=transferable_size, submission_succeeded_at=transfer_finished_at + ) + + factory.TransferableRangeFactory( + finished_at=transfer_finished_at, + outgoing_transferable=outgoing_transferable, + size=transferable_size, + transfer_state=origin_enums.TransferableRangeTransferState.TRANSFERRED, + ) + + queried_outgoing_transferable = models.OutgoingTransferable.objects.get( + id=outgoing_transferable.id + ) + + assert queried_outgoing_transferable.transfer_speed == expected_transfer_speed + + +@pytest.mark.django_db() +def test__build_transfer_speed_annotation_with_zero_transfer_duration( + faker: Faker, +): + """ + Test when `transfer_speed` is 0 (transfer has just been created) + """ + created_at = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + transfer_finished_at = None + transferable_size = 2 + + with freezegun.freeze_time(created_at): + outgoing_transferable = factory.OutgoingTransferableFactory( + size=transferable_size, submission_succeeded_at=transfer_finished_at + ) + + factory.TransferableRangeFactory( + finished_at=transfer_finished_at, + outgoing_transferable=outgoing_transferable, + size=transferable_size, + transfer_state=origin_enums.TransferableRangeTransferState.PENDING, + ) + + queried_outgoing_transferable = models.OutgoingTransferable.objects.get( + id=outgoing_transferable.id + ) + + assert queried_outgoing_transferable.transfer_speed is None + + +@pytest.mark.django_db() +def test__build_transfer_estimated_finish_date_annotation_not_none( + faker: Faker, +): + outgoing_transferable = factory.OutgoingTransferableFactory( + submission_succeeded_at=None, size=2 + ) + + time_to_freeze = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + + with freezegun.freeze_time(time_to_freeze): + queried_outgoing_transferable = ( + models.OutgoingTransferable.objects.annotate(transfer_speed=Value(1)) + .annotate( + transfer_estimated_finish_date=( + outgoing_transferable_model._build_transfer_estimated_finish_date_annotation() # noqa: E501 + ) + ) + .get(id=outgoing_transferable.id) + ) + + assert ( + queried_outgoing_transferable.transfer_estimated_finish_date + == time_to_freeze + datetime.timedelta(seconds=2) + ) + + +@pytest.mark.django_db() +def test__build_transfer_estimated_finish_date_annotation_transfer_speed_0(): + outgoing_transferable = factory.OutgoingTransferableFactory( + submission_succeeded_at=None + ) + + queried_outgoing_transferable = ( + models.OutgoingTransferable.objects.annotate(transfer_speed=Value(0)) + .annotate( + transfer_estimated_finish_date=( + outgoing_transferable_model._build_transfer_estimated_finish_date_annotation() # noqa: E501 + ) + ) + .get(id=outgoing_transferable.id) + ) + + assert queried_outgoing_transferable.transfer_estimated_finish_date is None + + +@pytest.mark.django_db() +def test__build_transfer_estimated_finish_date_annotation_submission_succeeded(): + outgoing_transferable = factory.OutgoingTransferableFactory( + submission_succeeded_at=timezone.now() + ) + + queried_outgoing_transferable = models.OutgoingTransferable.objects.annotate( + transfer_estimated_finish_date=( + outgoing_transferable_model._build_transfer_estimated_finish_date_annotation() # noqa: E501 + ) + ).get(id=outgoing_transferable.id) + + assert queried_outgoing_transferable.transfer_estimated_finish_date is None diff --git a/backend/tests/origin/integration/core/models/test_transferable_range.py b/backend/tests/origin/integration/core/models/test_transferable_range.py new file mode 100644 index 0000000..2bb3581 --- /dev/null +++ b/backend/tests/origin/integration/core/models/test_transferable_range.py @@ -0,0 +1,122 @@ +import freezegun +import pytest +from django.db import connection +from django.utils import timezone +from faker import Faker + +from eurydice.origin.core import enums +from eurydice.origin.core import models +from tests.origin.integration import factory + + +@pytest.mark.django_db() +def test_transferable_range_is_last_transferable_fully_submitted(faker: Faker): + """ + Assert TransferableRange.is_last correctly identifies the last TransferableRange + """ + + transferable_size = faker.pyint(min_value=100) + transferable_range_size = faker.pyint(min_value=1, max_value=transferable_size // 2) + + transferable = factory.OutgoingTransferableFactory( + size=transferable_size, + _submission_succeeded=True, + _submission_succeeded_at=faker.date_time_this_decade( + tzinfo=timezone.get_current_timezone() + ), + ) + + second_to_last_range = factory.TransferableRangeFactory( + byte_offset=transferable_size - transferable_range_size * 2, + size=transferable_range_size, + outgoing_transferable=transferable, + ) + + last_range = factory.TransferableRangeFactory( + byte_offset=transferable_size - transferable_range_size, + size=transferable_range_size, + outgoing_transferable=transferable, + ) + + assert last_range.is_last is True + assert second_to_last_range.is_last is False + + +@pytest.mark.django_db() +def test_transferable_range_is_last_shortcutting(faker: Faker): + """ + Assert TransferableRange.is_last correctly shortcuts + """ + + transferable_size = faker.pyint(min_value=100) + + transferable = factory.OutgoingTransferableFactory( + size=transferable_size, + _submission_succeeded=True, + _submission_succeeded_at=faker.date_time_this_decade( + tzinfo=timezone.get_current_timezone() + ), + ) + + only_range = factory.TransferableRangeFactory( + byte_offset=0, + size=transferable_size, + outgoing_transferable=transferable, + ) + + num_queries = len(connection.queries) + assert only_range.is_last is True + assert num_queries == len(connection.queries) + + +@pytest.mark.django_db() +def test_transferable_range_is_last_transferable_not_fully_submitted(): + """ + Assert TransferableRange.is_last correctly returns False if associated + Transferable is not finished uploading + """ + + transferable = factory.OutgoingTransferableFactory( + submission_succeeded_at=None, + ) + + a_transferable_range = factory.TransferableRangeFactory( + outgoing_transferable=transferable, + ) + + assert a_transferable_range.is_last is False + + +@pytest.mark.django_db() +def test_mark_as_transferred(): + transferable_range = factory.TransferableRangeFactory( + transfer_state=enums.TransferableRangeTransferState.PENDING, finished_at=None + ) + now = timezone.now() + with freezegun.freeze_time(now): + transferable_range.mark_as_transferred() + + queried_range = models.TransferableRange.objects.get(id=transferable_range.id) + + assert ( + queried_range.transfer_state == enums.TransferableRangeTransferState.TRANSFERRED + ) + assert queried_range.finished_at == now + + +def test_mark_as_finished_no_save_success(): + transferable_range = factory.TransferableRangeFactory.build( + transfer_state=enums.TransferableRangeTransferState.PENDING, finished_at=None + ) + + now = timezone.now() + with freezegun.freeze_time(now): + transferable_range._mark_as_finished( + enums.TransferableRangeTransferState.TRANSFERRED, save=False + ) + + assert ( + transferable_range.transfer_state + == enums.TransferableRangeTransferState.TRANSFERRED + ) + assert transferable_range.finished_at == now diff --git a/backend/tests/origin/integration/core/models/test_transferable_revocation.py b/backend/tests/origin/integration/core/models/test_transferable_revocation.py new file mode 100644 index 0000000..f034bcf --- /dev/null +++ b/backend/tests/origin/integration/core/models/test_transferable_revocation.py @@ -0,0 +1,47 @@ +import datetime + +import freezegun +import pytest +from django.utils import timezone +from faker import Faker + +from eurydice.origin.core import enums +from eurydice.origin.core import models +from tests.origin.integration import factory + + +@pytest.mark.django_db() +def test_list_pending_only_returns_pending_revocations(): + """ + Assert `list_pending` only returns `PENDING` revocations + """ + + for state in enums.TransferableRangeTransferState: + if state != enums.TransferableRangeTransferState.PENDING: + factory.TransferableRevocationFactory(transfer_state=state) + + assert not models.TransferableRevocation.objects.list_pending().exists() + + +@pytest.mark.django_db() +def test_list_pending_orders_by_creation_date(faker: Faker): + """ + Assert that revocations are correctly ordered by date + """ + + a_date = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + + with freezegun.freeze_time(a_date): + expected_second_revocation = factory.TransferableRevocationFactory( + transfer_state=enums.TransferableRangeTransferState.PENDING, + ) + + with freezegun.freeze_time(a_date - datetime.timedelta(seconds=1)): + expected_first_revocation = factory.TransferableRevocationFactory( + transfer_state=enums.TransferableRangeTransferState.PENDING, + ) + + revocations = models.TransferableRevocation.objects.list_pending() + + assert revocations.first() == expected_first_revocation + assert revocations.last() == expected_second_revocation diff --git a/backend/tests/origin/integration/core/models/test_user.py b/backend/tests/origin/integration/core/models/test_user.py new file mode 100644 index 0000000..8129fa5 --- /dev/null +++ b/backend/tests/origin/integration/core/models/test_user.py @@ -0,0 +1,28 @@ +from unittest import mock + +import pytest + +from eurydice.origin.core import models +from tests.origin.integration import factory + + +@pytest.mark.django_db() +def test_user_save_signal_create_userprofile(): + """ + Assert a UserProfile is created when a User is created + """ + + # Assert signal is called on model instance save + user = factory.UserFactoryWithSignal() + assert models.UserProfile.objects.filter(user__id=user.id).exists() + + # Assert signal is not called when model is updated + user.first_name = "Hubert" + user.last_name = "Bonisseur de La Bath" + + with mock.patch.object( + models.UserProfile.objects, "create", return_value=None + ) as mock_create: + user.save() + + assert not mock_create.called diff --git a/backend/tests/origin/integration/endpoints/__init__.py b/backend/tests/origin/integration/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/integration/endpoints/test_api_schema.py b/backend/tests/origin/integration/endpoints/test_api_schema.py new file mode 100644 index 0000000..766e415 --- /dev/null +++ b/backend/tests/origin/integration/endpoints/test_api_schema.py @@ -0,0 +1,14 @@ +import django.urls +import pytest +from rest_framework import status +from rest_framework import test + + +@pytest.mark.django_db() +def test_post_association_token_success(api_client: test.APIClient): + url = django.urls.reverse("api-schema") + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + assert response.headers["Content-Type"] == "application/json" + assert response.data["info"]["title"] == "Eurydice origin API" diff --git a/backend/tests/origin/integration/endpoints/test_metadata.py b/backend/tests/origin/integration/endpoints/test_metadata.py new file mode 100644 index 0000000..176b480 --- /dev/null +++ b/backend/tests/origin/integration/endpoints/test_metadata.py @@ -0,0 +1,9 @@ +import pytest +from rest_framework import test + +from tests.common.integration.endpoints import metadata + + +@pytest.mark.django_db() +def test_metadata(api_client: test.APIClient): + metadata.metadata(api_client) diff --git a/backend/tests/origin/integration/endpoints/test_metrics.py b/backend/tests/origin/integration/endpoints/test_metrics.py new file mode 100644 index 0000000..ce7fd3f --- /dev/null +++ b/backend/tests/origin/integration/endpoints/test_metrics.py @@ -0,0 +1,203 @@ +from datetime import datetime +from datetime import timedelta +from typing import List + +import dateutil +import freezegun +import pytest +from django import conf +from django.contrib.auth.models import Permission +from django.db.models import Q +from django.db.models import Sum +from django.urls import reverse +from django.utils import timezone +from faker import Faker +from rest_framework import test + +from eurydice.common.enums import OutgoingTransferableState as States +from eurydice.origin.api.views import metrics +from eurydice.origin.core.enums import TransferableRangeTransferState +from eurydice.origin.core.models import LastPacketSentAt +from eurydice.origin.core.models.outgoing_transferable import OutgoingTransferable +from eurydice.origin.core.models.transferable_range import TransferableRange +from tests.origin.integration import factory + + +def create_transferable(state: States, date: datetime) -> OutgoingTransferable: + with freezegun.freeze_time(date): + kwargs = { + "make_transferable_ranges_for_state": state, + } + if state == States.SUCCESS: + kwargs["_submission_succeeded"] = True + kwargs["make_transferable_ranges_for_state__finished_at"] = date + else: + kwargs["_submission_succeeded"] = False + return factory.OutgoingTransferableFactory(**kwargs) + + +def sum_pending_transferable_ranges_size( + outgoing_transferable: OutgoingTransferable, +) -> int: + return TransferableRange.objects.aggregate( + value=Sum( + "size", + filter=Q( + outgoing_transferable=outgoing_transferable, + transfer_state=TransferableRangeTransferState.PENDING, + ), + ) + )["value"] + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + ( + "old_states_repetitions", + "recent_states_repetitions", + ), + [ + ([2, 4, 6, 8, 10], [1, 3, 5, 7, 9]), + ([0, 0, 0, 0, 0], [0, 0, 0, 0, 0]), + ([0, 2, 0, 2, 0], [0, 2, 0, 2, 0]), + ([2, 0, 2, 0, 2], [2, 0, 2, 0, 2]), + ([1, 0, 4, 2, 0], [0, 4, 2, 0, 1]), + ], +) +def test__generate_metrics( + faker: Faker, + settings: conf.Settings, + api_client: test.APIClient, + old_states_repetitions: List[int], + recent_states_repetitions: List[int], +) -> None: + # Test preparation + + # We make sure the parameterization is consistent with the amount of + # known states. We use this data structure in parameterization because it + # is more concise (and readable) than a Dict[States, int] + assert len(old_states_repetitions) == len(States.values) + assert len(recent_states_repetitions) == len(States.values) + + old_transferable_nb = { + States.PENDING: old_states_repetitions[0], + States.ONGOING: old_states_repetitions[1], + States.ERROR: old_states_repetitions[2], + States.CANCELED: old_states_repetitions[3], + States.SUCCESS: old_states_repetitions[4], + } + + recent_transferable_nb = { + States.PENDING: recent_states_repetitions[0], + States.ONGOING: recent_states_repetitions[1], + States.ERROR: recent_states_repetitions[2], + States.CANCELED: recent_states_repetitions[3], + States.SUCCESS: recent_states_repetitions[4], + } + + settings.METRICS_SLIDING_WINDOW = 3600 + + # With the current fixed faker seed, faker just happens to return a datetime + # within 4000 seconds of a "spring forward", due to daylight saving time, + # which means that while the following code passes the test, it would fail + # if recent_date was using the fake datetime + # and old_date was computed using recent_date - 4000 seconds. + old_date = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + recent_date = old_date + timedelta(seconds=4000) + + expected = { + "queue_size": 0, + } + + for state, repetitions in old_transferable_nb.items(): + for _ in range(repetitions): + old_transferable = create_transferable(state=state, date=old_date) + + if state == States.ONGOING: + expected["queue_size"] += sum_pending_transferable_ranges_size( + old_transferable + ) + + for state, repetitions in recent_transferable_nb.items(): + for _ in range(repetitions): + recent_transferable = create_transferable(state=state, date=recent_date) + + if state == States.ONGOING: + expected["queue_size"] += sum_pending_transferable_ranges_size( + recent_transferable + ) + + expected["pending_transferables"] = ( + old_transferable_nb[States.PENDING] + recent_transferable_nb[States.PENDING] + ) + expected["ongoing_transferables"] = ( + old_transferable_nb[States.ONGOING] + recent_transferable_nb[States.ONGOING] + ) + expected["recent_successes"] = recent_transferable_nb[States.SUCCESS] + expected["recent_errors"] = recent_transferable_nb[States.ERROR] + expected["last_packet_sent_at"] = None + + user_profile = factory.UserProfileFactory() + permission = Permission.objects.get(codename="view_rolling_metrics") + user_profile.user.user_permissions.add(permission) + + with freezegun.freeze_time(recent_date): + assert metrics.MetricsView.get_object(None) == expected + + api_client.force_authenticate(user_profile.user) + url = reverse("metrics") + response = api_client.get(url) + assert response.json() == expected + + +@pytest.mark.django_db() +def test__last_packet_sent_at( + faker: Faker, + api_client: test.APIClient, +): + # Test preparation + LastPacketSentAt.update() + + timestamp = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + with freezegun.freeze_time(timestamp): + LastPacketSentAt.update() + + user_profile = factory.UserProfileFactory() + permission = Permission.objects.get(codename="view_rolling_metrics") + user_profile.user.user_permissions.add(permission) + api_client.force_authenticate(user_profile.user) + + # Actual test + url = reverse("metrics") + response = api_client.get(url) + response_json = response.json() + assert dateutil.parser.parse(response_json["last_packet_sent_at"]) == timestamp + + +@pytest.mark.django_db() +def test__metrics_permissions_deny(api_client: test.APIClient): + user_profile = factory.UserProfileFactory() + url = reverse("metrics") + + response = api_client.get(url) + + assert response.status_code == 401 + + api_client.force_authenticate(user_profile.user) + response = api_client.get(url) + + assert response.status_code == 403 + assert "You do not have permission" in response.json()["detail"] + + +@pytest.mark.django_db() +def test__metrics_permissions_allow(api_client: test.APIClient): + user_profile = factory.UserProfileFactory() + permission = Permission.objects.get(codename="view_rolling_metrics") + user_profile.user.user_permissions.add(permission) + + api_client.force_authenticate(user_profile.user) + url = reverse("metrics") + response = api_client.get(url) + + assert response.status_code == 200 diff --git a/backend/tests/origin/integration/endpoints/test_outgoing_transferable.py b/backend/tests/origin/integration/endpoints/test_outgoing_transferable.py new file mode 100644 index 0000000..8b20ee1 --- /dev/null +++ b/backend/tests/origin/integration/endpoints/test_outgoing_transferable.py @@ -0,0 +1,77 @@ +import io +from typing import Union +from unittest import mock + +import pytest +from django import conf +from faker import Faker + +from eurydice.origin.api.views import outgoing_transferable +from eurydice.origin.core import models +from tests.origin.integration import factory + + +@pytest.mark.django_db() +@pytest.mark.parametrize("nb_ranges", [1, 2]) +@mock.patch( + "eurydice.origin.api.views.outgoing_transferable._finalize_transferable", + side_effect=RuntimeError("Something terrible happened!"), +) +def test__perform_create_transferable_ranges_error_rollback( + mocked_finalize_transferable: mock.Mock, + nb_ranges: int, + faker: Faker, + settings: conf.Settings, +): + settings.TRANSFERABLE_RANGE_SIZE = 1 + data = faker.binary(length=settings.TRANSFERABLE_RANGE_SIZE * nb_ranges) + transferable = factory.OutgoingTransferableFactory( + sha1=None, bytes_received=0, size=len(data), submission_succeeded_at=None + ) + + with pytest.raises(RuntimeError, match="Something terrible happened!"): + outgoing_transferable._perform_create_transferable_ranges( + transferable=transferable, stream=io.BytesIO(data) + ) + + transferable.refresh_from_db() + assert ( + transferable.bytes_received + == (nb_ranges - 1) * settings.TRANSFERABLE_RANGE_SIZE + ) + assert transferable.sha1 is None + assert models.TransferableRange.objects.count() == nb_ranges - 1 + + mocked_finalize_transferable.assert_called_once() + + +@pytest.mark.django_db() +@pytest.mark.parametrize("provide_size", [False, True]) +@pytest.mark.parametrize("data_size", ["TRANSFERABLE_RANGE_SIZE", 0]) +def test__perform_create_transferable_ranges( + data_size: Union[str, int], + provide_size: bool, + faker: Faker, + settings: conf.Settings, +): + settings.TRANSFERABLE_RANGE_SIZE = 256 + + if data_size == "TRANSFERABLE_RANGE_SIZE": + data_size = settings.TRANSFERABLE_RANGE_SIZE + + data = faker.binary(data_size) + transferable = factory.OutgoingTransferableFactory( + sha1=None, + bytes_received=0, + size=len(data) if provide_size else None, + submission_succeeded_at=None, + ) + + outgoing_transferable._perform_create_transferable_ranges( + transferable=transferable, stream=io.BytesIO(data) + ) + + transferable.refresh_from_db() + assert transferable.bytes_received == data_size + assert transferable.size == len(data) + assert models.TransferableRange.objects.count() == 1 diff --git a/backend/tests/origin/integration/endpoints/test_pagination.py b/backend/tests/origin/integration/endpoints/test_pagination.py new file mode 100644 index 0000000..2b4303c --- /dev/null +++ b/backend/tests/origin/integration/endpoints/test_pagination.py @@ -0,0 +1,30 @@ +from typing import Optional + +from eurydice.common.enums import OutgoingTransferableState +from eurydice.origin.core.models import OutgoingTransferable +from tests.common.integration.endpoints import pagination +from tests.origin.integration import factory + + +def make_transferables( + count: int, state: Optional[OutgoingTransferableState] = None, **kwargs +): + factory.OutgoingTransferableFactory.create_batch( + count, make_transferable_ranges_for_state=state, **kwargs + ) + + +class TestPagination(pagination.PaginationTestsSuperclass): + user_profile_factory = factory.UserProfileFactory + transferable_class = OutgoingTransferable + success_state = OutgoingTransferableState.SUCCESS + error_state = OutgoingTransferableState.ERROR + make_transferables = make_transferables + + +class TestPaginationInTransaction(pagination.PaginationTestsInTransactionSuperclass): + user_profile_factory = factory.UserProfileFactory + transferable_class = OutgoingTransferable + success_state = OutgoingTransferableState.SUCCESS + error_state = OutgoingTransferableState.ERROR + make_transferables = make_transferables diff --git a/backend/tests/origin/integration/endpoints/test_status.py b/backend/tests/origin/integration/endpoints/test_status.py new file mode 100644 index 0000000..70228ea --- /dev/null +++ b/backend/tests/origin/integration/endpoints/test_status.py @@ -0,0 +1,72 @@ +import dateutil +import freezegun +import pytest +from django.urls import reverse +from django.utils import timezone +from faker import Faker +from rest_framework import status +from rest_framework import test + +from eurydice.origin.core import models +from tests.origin.integration import factory + + +@pytest.mark.django_db() +def test_get_metrics_permissions_deny(api_client: test.APIClient): + url = reverse("metrics") + response = api_client.get(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + +@pytest.mark.django_db() +def test_get_status__no_maintenance_no_packet_sent( + api_client: test.APIClient, +) -> None: + user_profile = factory.UserProfileFactory() + api_client.force_authenticate(user_profile.user) + + url = reverse("status") + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + assert response.data == { + "maintenance": False, + "last_packet_sent_at": None, + } + + +@pytest.mark.django_db() +def test_get_status__maintenance_with_packet_timestamp( + faker: Faker, + api_client: test.APIClient, +) -> None: + maintenance = models.Maintenance.objects.get() + maintenance.maintenance = True + maintenance.save() + + timestamp = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + with freezegun.freeze_time(timestamp): + models.LastPacketSentAt.update() + + user_profile = factory.UserProfileFactory() + api_client.force_authenticate(user_profile.user) + + url = reverse("status") + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + response_json = response.json() + assert len(response_json) == 2 + assert response_json["maintenance"] is True + assert dateutil.parser.parse(response_json["last_packet_sent_at"]) == timestamp + + +@pytest.mark.django_db() +def test_get_status_permissions_deny( + api_client: test.APIClient, +) -> None: + url = reverse("status") + response = api_client.get(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED diff --git a/backend/tests/origin/integration/endpoints/test_transferables.py b/backend/tests/origin/integration/endpoints/test_transferables.py new file mode 100644 index 0000000..3d52225 --- /dev/null +++ b/backend/tests/origin/integration/endpoints/test_transferables.py @@ -0,0 +1,595 @@ +import hashlib +import math +from itertools import product +from typing import Any +from unittest import mock + +import dateutil.parser +import pytest +from django import conf +from django.db.models.query import Prefetch +from django.test import RequestFactory +from django.urls import reverse +from django.utils import timezone +from faker import Faker +from freezegun import freeze_time +from rest_framework import status +from rest_framework import test +from rest_framework.authtoken.models import Token + +from eurydice.common import enums +from eurydice.common import minio +from eurydice.origin.api.views import OutgoingTransferableViewSet +from eurydice.origin.core import enums as origin_enums +from eurydice.origin.core import models +from tests.origin.integration import factory as models_factory + + +@pytest.fixture() +def _s3_bucket(faker: Faker, settings: conf.Settings) -> Any: + bucket_name = f"test-{faker.pystr().lower()}" + settings.MINIO_BUCKET_NAME = bucket_name + + minio.client.make_bucket(bucket_name=bucket_name) + + yield + + for s3_obj in minio.client.list_objects(bucket_name=bucket_name): + minio.client.remove_object( + bucket_name=bucket_name, object_name=s3_obj.object_name + ) + + minio.client.remove_bucket(bucket_name=bucket_name) + + +def _api_client_session_auth(api_client: test.APIClient, user: models.User) -> None: + api_client.force_login(user) + + +def _api_client_token_auth(api_client: test.APIClient, user: models.User) -> None: + token = Token.objects.create(user=user) + + api_client.credentials(HTTP_AUTHORIZATION=f"Token {token.key}") + + +class TestOutgoingTransferableInTransaction(test.APITransactionTestCase): + def test_destroy_outgoing_transferable_error_not_authorized( + self, + ): + obj = models_factory.OutgoingTransferableFactory() + another_user = models_factory.UserFactory() + + self.client.force_login(user=another_user) + url = reverse("transferable-detail", kwargs={"pk": obj.id}) + response = self.client.delete(url) + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.data["detail"].code == "not_found" + + def test_retrieve_outgoing_transferable_error_not_authorized( + self, + ): + obj = models_factory.OutgoingTransferableFactory() + another_user = models_factory.UserFactory() + + self.client.force_login(user=another_user) + url = reverse("transferable-detail", kwargs={"pk": obj.id}) + response = self.client.get(url) + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.data["detail"].code == "not_found" + + def test_create_outgoing_transferable_empty_upload_wrong_content_length( + self, + ): + user_profile = models_factory.UserProfileFactory() + self.client.force_login(user=user_profile.user) + + url = reverse("transferable-list") + response = self.client.post( + url, + data=b"", + content_type="application/octet-stream", + HTTP_CONTENT_LENGTH="42", + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + def test_create_outgoing_transferable_invalid_content_length_header( + self, + ): + user_profile = models_factory.UserProfileFactory() + self.client.force_login(user=user_profile.user) + + url = reverse("transferable-list") + response = self.client.post( + url, + data=b"0" * 1024, + content_type="application/octet-stream", + HTTP_CONTENT_LENGTH="invalid-content-length-value", + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert str(response.data["detail"]) == "Invalid value for Content-Length header" + + # NOTE: We test both authentication methods because, + # for some reason, DRF's parsers are ignored + # on the OutgoingTransferable view when using + # DRF's TokenAuthentication. + def test_create_outgoing_transferable_unsupported_content_type_api_client(self): + authentication_methods = [_api_client_token_auth, _api_client_session_auth] + content_types = [ + "application/x-www-form-urlencoded", + 'multipart/form-data;boundary="boundary"', + ] + for authentication_method, content_type in product( + authentication_methods, content_types + ): + with self.subTest(): + user_profile = models_factory.UserProfileFactory() + + url = reverse("transferable-list") + + authentication_method(self.client, user_profile.user) + + response = self.client.post( + url, content_type=content_type, data={"file": "Hello, world"} + ) + + assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE + assert response.data["detail"].code == "unsupported_media_type" + + +@pytest.mark.django_db() +class TestOutgoingTransferable: + def test_list_outgoing_transferables(self, api_client: test.APIClient): + created_obj = models_factory.OutgoingTransferableFactory() + + obj = models.OutgoingTransferable.objects.get(id=created_obj.id) + + url = reverse("transferable-list") + api_client.force_login(user=obj.user_profile.user) + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 1 + assert len(response.data["results"]) == 1 + + data = response.data["results"][0] + assert data["name"] == obj.name + if obj.sha1 is not None: + assert bytes.fromhex(data["sha1"]) == bytes(obj.sha1) + else: + assert data["sha1"] is None + assert data["size"] == obj.size + assert data["progress"] == 0 + assert data["user_provided_meta"] == obj.user_provided_meta + assert data["state"] == obj.state + + if obj.submission_succeeded_at is not None: + assert ( + dateutil.parser.parse(data["submission_succeeded_at"]) + == obj.submission_succeeded_at + ) + + def test_retrieve_outgoing_transferable(self, api_client: test.APIClient): + created_obj = models_factory.OutgoingTransferableFactory( + submission_succeeded_at=timezone.now() + ) + + obj = models.OutgoingTransferable.objects.get(id=created_obj.id) + + url = reverse("transferable-detail", kwargs={"pk": obj.id}) + api_client.force_login(user=obj.user_profile.user) + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + data = response.data + assert data["name"] == obj.name + assert bytes.fromhex(data["sha1"]) == bytes(obj.sha1) + assert data["size"] == obj.size + assert data["progress"] == 0 + assert data["user_provided_meta"] == obj.user_provided_meta + assert data["state"] == obj.state + if obj.submission_succeeded_at is not None: + assert ( + dateutil.parser.parse(data["submission_succeeded_at"]) + == obj.submission_succeeded_at + ) + + def test_retrieve_outgoing_transferable_error_not_authenticated( + self, api_client: test.APIClient + ): + obj = models_factory.OutgoingTransferableFactory() + url = reverse("transferable-detail", kwargs={"pk": obj.id}) + response = api_client.get(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response.data["detail"].code == "not_authenticated" + + def test_list_outgoing_transferable_error_not_authenticated( + self, api_client: test.APIClient + ): + url = reverse("transferable-list") + response = api_client.get(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response.data["detail"].code == "not_authenticated" + + def test_list_outgoing_transferable_only_own_transferables_visible( + self, api_client: test.APIClient + ): + models_factory.OutgoingTransferableFactory() + another_user = models_factory.UserFactory() + + api_client.force_login(user=another_user) + url = reverse("transferable-list") + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 0 + assert response.data["results"] == [] + + @pytest.mark.usefixtures("_s3_bucket") + @pytest.mark.parametrize( + ("transferable_size", "transferable_range_size"), + [ + # test when the len(transferable) == transferable_range_size + (10, 10), + # test when the len(transferable) > transferable_range_size + (10, 5), + # test when len(transferable) < transferable_range_size + (5, 10), + # test when len(transferable) > transferable_range_size + # and non proportional transferable_range_size + (10, 3), + (0, 1), + ], + ) + def test_create_outgoing_transferable( + self, + transferable_size: int, + transferable_range_size: int, + api_client: test.APIClient, + settings: conf.Settings, + faker: Faker, + ): + """Upload a file to the origin API. + Assert DB entries are correctly created. + Assert the S3 objects contain the correct file. + + Args: + transferable_size (int): size of the random bytes to upload + transferable_range_size (int): overridden django settings eurydice parameter + """ + user_profile = models_factory.UserProfileFactory() + + file_path = faker.file_path() + + random_bytes = faker.binary(length=transferable_size) + + url = reverse("transferable-list") + + api_client.force_login(user=user_profile.user) + + settings.TRANSFERABLE_RANGE_SIZE = transferable_range_size + + now = timezone.now() + with freeze_time(now): + # post file + response = api_client.post( + url, + data=random_bytes, + content_type="application/octet-stream", + HTTP_METADATA_PATH=file_path, + ) + + data = response.data + + assert dateutil.parser.parse(data["created_at"]) == now + + assert bytes.fromhex(data["sha1"]) == hashlib.sha1(random_bytes).digest() + assert data["size"] == len(random_bytes) + assert dateutil.parser.parse(data["submission_succeeded_at"]) == now + assert data["state"] == enums.OutgoingTransferableState.PENDING.value + assert data["user_provided_meta"] == {"Metadata-Path": file_path} + assert data["progress"] == 0 + assert data["bytes_transferred"] == 0 + assert data["transfer_finished_at"] is None + assert data["transfer_speed"] is None + assert data["transfer_estimated_finish_date"] is None + + outgoing_transferable = models.OutgoingTransferable.objects.prefetch_related( + Prefetch( + "transferable_ranges", + queryset=models.TransferableRange.objects.order_by("byte_offset"), + ) + ).get(id=data["id"]) + + if transferable_size == 0: + expected_transferable_range_count = 1 + else: + expected_transferable_range_count = math.ceil( + transferable_size / transferable_range_size + ) + + assert ( + len(outgoing_transferable.transferable_ranges.all()) + == expected_transferable_range_count + ) + + # Verify uploaded transferable_ranges + final_transferable_digest = hashlib.sha1() + for transferable_range in outgoing_transferable.transferable_ranges.all(): + response = None + try: + response = minio.client.get_object( + bucket_name=transferable_range.s3_bucket_name, + object_name=transferable_range.s3_object_name, + ) + final_transferable_digest.update(response.read()) + finally: + if response: + response.close() + response.release_conn() + + def test_create_outgoing_transferable_unauthorized( + self, faker: Faker, api_client: test.APIClient + ): + """ + Assert only authenticated users can create Transferables + """ + random_bytes = faker.binary(length=5) + + url = reverse("transferable-list") + + # post file + response = api_client.post( + url, + data=random_bytes, + content_type="application/octet-stream", + ) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + @pytest.mark.django_db(transaction=True) + def test_create_outgoing_transferable_too_large( + self, + api_client: test.APIClient, + settings: conf.Settings, + faker: Faker, + ): + """ + Assert HTTP 413 is raised when trying to create a Transferable that is too large + """ + + user_profile = models_factory.UserProfileFactory() + + api_client.force_login(user=user_profile.user) + + url = reverse("transferable-list") + + # post file with a "Content-Length: 2" header + settings.TRANSFERABLE_MAX_SIZE = 1 + response = api_client.post( + url, content_type="application/octet-stream", HTTP_CONTENT_LENGTH="2" + ) + + assert response.status_code == status.HTTP_413_REQUEST_ENTITY_TOO_LARGE + assert response.data["detail"].code == "RequestEntityTooLargeError" + + # post file with a "Transfer-Encoding: chunked" header + settings.TRANSFERABLE_RANGE_SIZE = 1 + response = api_client.post( + url, + content_type="application/octet-stream", + data=faker.binary(length=2), + HTTP_TRANSFER_ENCODING="chunked", + ) + + assert response.status_code == status.HTTP_413_REQUEST_ENTITY_TOO_LARGE + assert response.data["detail"].code == "RequestEntityTooLargeError" + + @mock.patch( + "eurydice.origin.api.views.outgoing_transferable._create_transferable_ranges" + ) + def test_create_outgoing_transferable_unexpected_exception( + self, + mocked_create_transferable_ranges: mock.Mock, + ): + assert not models.OutgoingTransferable.objects.exists() + assert not models.TransferableRevocation.objects.exists() + + mocked_create_transferable_ranges.side_effect = RuntimeError( + "Something bad happened!" + ) + + user_profile = models_factory.UserProfileFactory() + + api_client = test.APIClient(raise_request_exception=False) + api_client.force_login(user=user_profile.user) + url = reverse("transferable-list") + + response = api_client.post( + url, + data=b"0" * 1024, + content_type="application/octet-stream", + ) + + assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + + outgoing_transferable = models.OutgoingTransferable.objects.get() + assert outgoing_transferable.state == enums.OutgoingTransferableState.ERROR + assert models.TransferableRevocation.objects.filter( + reason=enums.TransferableRevocationReason.UNEXPECTED_EXCEPTION, + outgoing_transferable=outgoing_transferable.id, + ).exists() + + def test_create_outgoing_transferable_empty_content_length_header( + self, + api_client: test.APIClient, + ): + assert not models.OutgoingTransferable.objects.exists() + + user_profile = models_factory.UserProfileFactory() + api_client.force_login(user=user_profile.user) + + url = reverse("transferable-list") + response = api_client.post( + url, + data=b"0" * 1024, + content_type="application/octet-stream", + HTTP_CONTENT_LENGTH="", + ) + + assert response.status_code == status.HTTP_201_CREATED + + def test_create_outgoing_transferable_missing_content_type(self): + url = reverse("transferable-list") + user_profile = models_factory.UserProfileFactory() + + request_factory = RequestFactory() + + token = Token.objects.create(user=user_profile.user) + + request = request_factory.post( + url, data={}, HTTP_AUTHORIZATION=f"Token {token.key}" + ) + + request.META.pop("CONTENT_TYPE", None) + + view = OutgoingTransferableViewSet.as_view({"post": "create"}) + + response = view(request) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data["detail"].code == "MissingContentTypeError" + + def test_destroy_outgoing_transferable_success(self, api_client: test.APIClient): + obj = models_factory.OutgoingTransferableFactory( + _submission_succeeded=True, _submission_succeeded_at=timezone.now() + ) + models_factory.TransferableRangeFactory( + outgoing_transferable=obj, + transfer_state=origin_enums.TransferableRangeTransferState.PENDING, + ) + obj = models.OutgoingTransferable.objects.get(id=obj.id) + + assert obj.state == enums.OutgoingTransferableState.PENDING + + url = reverse("transferable-detail", kwargs={"pk": obj.id}) + api_client.force_login(user=obj.user_profile.user) + response = api_client.delete(url) + + assert response.status_code == status.HTTP_204_NO_CONTENT + + obj = models.OutgoingTransferable.objects.get(id=obj.id) + assert obj.state == enums.OutgoingTransferableState.CANCELED + + def test_destroy_outgoing_transferable_error_not_authenticated( + self, api_client: test.APIClient + ): + obj = models_factory.OutgoingTransferableFactory() + url = reverse("transferable-detail", kwargs={"pk": obj.id}) + response = api_client.delete(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response.data["detail"].code == "not_authenticated" + + def test_destroy_outgoing_transferable_error_not_successfully_submitted( + self, api_client: test.APIClient + ): + obj = models_factory.OutgoingTransferableFactory(submission_succeeded_at=None) + + assert not obj.submission_succeeded + + api_client.force_login(user=obj.user_profile.user) + url = reverse("transferable-detail", kwargs={"pk": obj.id}) + response = api_client.delete(url) + + assert response.status_code == status.HTTP_204_NO_CONTENT + + revocations = models.TransferableRevocation.objects.filter( + outgoing_transferable=obj + ) + + assert revocations.count() == 1 + assert ( + revocations.get().reason == enums.TransferableRevocationReason.USER_CANCELED + ) + + +@pytest.mark.parametrize("transferable_range_size", [1, 2]) +@pytest.mark.parametrize( + ( + "content_length", + "transferable_size", + "expected_status_code", + "expected_revocation_count", + "expected_error_detail", + ), + [ + # Test when content length == transferable size == 0 + (0, 0, status.HTTP_201_CREATED, 0, None), + # Test when content length is 0 + (0, 2, status.HTTP_400_BAD_REQUEST, 1, "InconsistentContentLengthError"), + # Test when content length < transferable size + (1, 2, status.HTTP_400_BAD_REQUEST, 1, "InconsistentContentLengthError"), + # Test when content length > transferable size + (3, 2, status.HTTP_400_BAD_REQUEST, 1, "InconsistentContentLengthError"), + # Test when content length == transferable size + (2, 2, status.HTTP_201_CREATED, 0, None), + # Test when content length is not given + (None, 2, status.HTTP_201_CREATED, 0, None), + # Test when content length given is just an empty string + # (it is sometimes added like that by gunicorn) + ("", 2, status.HTTP_201_CREATED, 0, None), + ], +) +@pytest.mark.django_db(transaction=True) +def test_create_outgoing_transferable_content_length( + content_length: int, + transferable_size: int, + expected_status_code: int, + transferable_range_size: int, + expected_revocation_count: int, + expected_error_detail: str, + settings: conf.Settings, + faker: Faker, +): + """ + Assert HTTP 400 is raised when trying to create a Transferable + with an incorrect Content-Length header + """ + settings.TRANSFERABLE_RANGE_SIZE = transferable_range_size + + random_bytes = faker.binary(length=transferable_size) + + user_profile = models_factory.UserProfileFactory() + + client = test.APIClient() + + client.force_login(user_profile.user) + + url = reverse("transferable-list") + + request_kwargs = { + "content_type": "application/octet-stream", + "data": random_bytes, + } + + if content_length is not None: + request_kwargs["HTTP_CONTENT_LENGTH"] = content_length + + response = client.post(url, **request_kwargs) + + assert response.status_code == expected_status_code + + revocations = models.TransferableRevocation.objects.filter( + outgoing_transferable__user_profile=user_profile + ) + + assert len(revocations) == expected_revocation_count + + if response.status_code != status.HTTP_201_CREATED: + assert response.data["detail"].code == expected_error_detail diff --git a/backend/tests/origin/integration/endpoints/test_transferables_filtering.py b/backend/tests/origin/integration/endpoints/test_transferables_filtering.py new file mode 100644 index 0000000..eee313e --- /dev/null +++ b/backend/tests/origin/integration/endpoints/test_transferables_filtering.py @@ -0,0 +1,256 @@ +import hashlib +from datetime import datetime +from datetime import timedelta + +import freezegun +import pytest +from django.urls import reverse +from django.utils import timezone +from django.utils.dateparse import parse_datetime +from faker import Faker +from rest_framework import status +from rest_framework import test + +from eurydice.origin.core import enums +from tests.origin.integration import factory + + +def _fdate(date: datetime) -> str: + return date.isoformat()[:-6] + "Z" + + +@pytest.mark.django_db() +class TestOutgoingTransferableFiltering: + def test_filter_creation_date( + self, + api_client: test.APIClient, + faker: Faker, + ): + user_profile = factory.UserProfileFactory() + + a_date = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + ref_date = a_date + timedelta(days=2) + + for days in (0, 1, 3, 4): + with freezegun.freeze_time(a_date + timedelta(days=days)): + factory.OutgoingTransferableFactory(user_profile=user_profile) + + api_client.force_login(user_profile.user) + url = reverse("transferable-list") + response = api_client.get(url, {"created_after": _fdate(ref_date)}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 2 + assert len(response.data["results"]) == 2 + + for data in response.data["results"]: + assert parse_datetime(data["created_at"]) > ref_date + + response = api_client.get( + url, + { + "created_after": _fdate(ref_date), + "page": response.data["pages"]["current"], + }, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 2 + assert len(response.data["results"]) == 2 + + def test_filter_state( + self, + api_client: test.APIClient, + ): + user_profile = factory.UserProfileFactory() + api_client.force_login(user_profile.user) + + for state in enums.TransferableRangeTransferState.names: + factory.TransferableRangeFactory( + transfer_state=state, + outgoing_transferable=factory.OutgoingTransferableFactory( + user_profile=user_profile, _submission_succeeded=True + ), + ) + + for state in ("PENDING", "ONGOING", "ERROR", "SUCCESS"): + url = reverse("transferable-list") + response = api_client.get(url, {"state": state}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 1 + assert len(response.data["results"]) == 1 + + for data in response.data["results"]: + assert data["state"] == state + + def test_filter_name( + self, + api_client: test.APIClient, + ): + user_profile = factory.UserProfileFactory() + api_client.force_login(user_profile.user) + + for name in ("aaa.txt", "bbb.txt", "aaa.bin", "txt.aaa", "ccc.txt"): + factory.OutgoingTransferableFactory(user_profile=user_profile, name=name) + + url = reverse("transferable-list") + response = api_client.get(url, {"name": ".txt"}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 3 + assert len(response.data["results"]) == 3 + + for data in response.data["results"]: + assert data["name"].endswith(".txt") + + def test_filter_sha1( + self, + api_client: test.APIClient, + ): + user_profile = factory.UserProfileFactory() + api_client.force_login(user_profile.user) + + for name in ("aaa.txt", "bbb.txt", "aaa.bin", "txt.aaa", "ccc.txt"): + factory.OutgoingTransferableFactory( + user_profile=user_profile, + name=name, + submission_succeeded_at=timezone.now(), + sha1=hashlib.sha1(name.encode("utf-8")).digest(), + ) + + url = reverse("transferable-list") + response = api_client.get(url, {"sha1": hashlib.sha1(b"txt.aaa").hexdigest()}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 1 + assert len(response.data["results"]) == 1 + + for data in response.data["results"]: + assert data["name"] == "txt.aaa" + + def test_filter_submission_succeeded( + self, + api_client: test.APIClient, + faker: Faker, + ): + user_profile = factory.UserProfileFactory() + + a_date = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + + for days in (-6, -1, 2, 1): + factory.OutgoingTransferableFactory( + user_profile=user_profile, + _submission_succeeded=True, + _submission_succeeded_at=a_date + timedelta(days=days), + ) + + api_client.force_login(user_profile.user) + url = reverse("transferable-list") + response = api_client.get(url, {"submission_succeeded_before": _fdate(a_date)}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 2 + assert len(response.data["results"]) == 2 + + for data in response.data["results"]: + assert parse_datetime(data["submission_succeeded_at"]) < a_date + + def test_filter_transfer_finished( + self, + api_client: test.APIClient, + faker: Faker, + ): + user_profile = factory.UserProfileFactory() + + a_date = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + + with freezegun.freeze_time(a_date + timedelta(days=-1000)): + for days in (5, 2, -9, 1): + factory.TransferableRangeFactory( + transfer_state=enums.TransferableRangeTransferState.TRANSFERRED, + size=42, + byte_offset=0, + finished_at=a_date + timedelta(days=days), + outgoing_transferable=factory.OutgoingTransferableFactory( + user_profile=user_profile, _submission_succeeded=True, size=42 + ), + ) + + api_client.force_login(user_profile.user) + url = reverse("transferable-list") + response = api_client.get(url, {"transfer_finished_after": _fdate(a_date)}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 3 + assert len(response.data["results"]) == 3 + + for data in response.data["results"]: + assert parse_datetime(data["transfer_finished_at"]) > a_date + + def test_page_size(self, api_client: test.APIClient): + user_profile = factory.UserProfileFactory() + factory.OutgoingTransferableFactory.create_batch(55, user_profile=user_profile) + + api_client.force_login(user_profile.user) + url = reverse("transferable-list") + + response = api_client.get(url, {"page_size": 25}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 55 + assert len(response.data["results"]) == 25 + + response = api_client.get(url, {"page_size": 20}) + response = api_client.get( + url, + {"page_size": 20, "delta": 2, "from": response.data["pages"]["current"]}, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 55 + assert len(response.data["results"]) == 15 + + def test_isodate( + self, + api_client: test.APIClient, + faker: Faker, + ): + user_profile = factory.UserProfileFactory() + + a_date = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + + with freezegun.freeze_time(a_date): + factory.OutgoingTransferableFactory(user_profile=user_profile) + + # check if the returned date is the exact creation date (same timezone) + api_client.force_login(user_profile.user) + url = reverse("transferable-list") + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 1 + assert len(response.data["results"]) == 1 + + retrieved_str_date = response.data["results"][0]["created_at"] + retrieved_date = parse_datetime(retrieved_str_date) + + assert a_date == retrieved_date + + # check if comparison with the same format than the input is OK : after + api_client.force_login(user_profile.user) + url = reverse("transferable-list") + response = api_client.get(url, {"created_after": retrieved_str_date}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 1 + assert len(response.data["results"]) == 1 + + # check if comparison with the same format than the input is OK : before + api_client.force_login(user_profile.user) + url = reverse("transferable-list") + response = api_client.get(url, {"created_before": retrieved_str_date}) + + assert response.status_code == status.HTTP_200_OK + assert response.data["count"] == 1 + assert len(response.data["results"]) == 1 diff --git a/backend/tests/origin/integration/endpoints/test_user.py b/backend/tests/origin/integration/endpoints/test_user.py new file mode 100644 index 0000000..c97af89 --- /dev/null +++ b/backend/tests/origin/integration/endpoints/test_user.py @@ -0,0 +1,81 @@ +import django.urls +import freezegun +import pytest +from django.conf import settings +from django.utils import timezone +from faker import Faker +from rest_framework import status +from rest_framework import test +from rest_framework.authtoken.models import Token + +from eurydice.common.api import serializers +from tests.origin.integration import factory + + +@pytest.mark.django_db() +class TestUser: + def test_get_association_token(self, api_client: test.APIClient) -> None: + user_profile = factory.UserProfileFactory() + + api_client.force_login(user=user_profile.user) + url = django.urls.reverse("user-association") + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + assert {"token", "expires_at"} == response.data.keys() + + serializer = serializers.AssociationTokenSerializer(data=response.data) + assert serializer.is_valid() + + deserialized_token = serializer.validated_data + assert deserialized_token.user_profile_id == user_profile.id + + def test_get_association_token_error_not_authenticated( + self, api_client: test.APIClient + ) -> None: + url = django.urls.reverse("user-association") + response = api_client.get(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response.data["detail"].code == "not_authenticated" + + def test_api_call_updates_user_last_access__remote_user( + self, + api_client: test.APIClient, + faker: Faker, + ): + user = factory.UserFactory() + api_client.credentials(**{settings.REMOTE_USER_HEADER: user.username}) + + assert user.last_access is None + + url = django.urls.reverse("user-login") + + a_date = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + with freezegun.freeze_time(a_date): + response = api_client.get(url) + assert response.status_code == 204 + + user.refresh_from_db() + assert user.last_access == a_date + + def test_api_call_updates_user_last_access__token( + self, + api_client: test.APIClient, + faker: Faker, + ): + user = factory.UserFactory() + token = Token.objects.create(user=user) + api_client.credentials(HTTP_AUTHORIZATION=f"Token {token.key}") + + assert user.last_access is None + + url = django.urls.reverse("transferable-list") + + a_date = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + with freezegun.freeze_time(a_date): + response = api_client.get(url) + assert response.status_code == 200 + + user.refresh_from_db() + assert user.last_access == a_date diff --git a/backend/tests/origin/integration/endpoints/test_user_details.py b/backend/tests/origin/integration/endpoints/test_user_details.py new file mode 100644 index 0000000..403067c --- /dev/null +++ b/backend/tests/origin/integration/endpoints/test_user_details.py @@ -0,0 +1,26 @@ +import django.urls +import pytest +from rest_framework import status +from rest_framework import test + +from tests.origin.integration import factory + + +@pytest.mark.django_db() +def user_details_returns_username(api_client: test.APIClient): + user = factory.UserFactory() + api_client.force_login(user=user) + url = django.urls.reverse("user-details") + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + assert response.data["username"] == user.username + + +@pytest.mark.django_db() +def user_details_returns_401_when_not_authenticated(api_client: test.APIClient): + url = django.urls.reverse("user-details") + response = api_client.get(url) + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert not response.has_header("Authenticated-User") diff --git a/backend/tests/origin/integration/endpoints/test_user_login.py b/backend/tests/origin/integration/endpoints/test_user_login.py new file mode 100644 index 0000000..fe76313 --- /dev/null +++ b/backend/tests/origin/integration/endpoints/test_user_login.py @@ -0,0 +1,27 @@ +import pytest +from faker import Faker +from rest_framework import test + +from tests.common.integration.endpoints import login +from tests.origin.integration import factory + + +@pytest.mark.django_db() +def test_user_login_sets_cookies_with_default_values(api_client: test.APIClient): + login.user_login_sets_cookies_with_default_values(api_client) + + +@pytest.mark.django_db() +def test_user_login_forbidden_without_remote_user_header(api_client: test.APIClient): + login.user_login_forbidden_without_remote_user_header(api_client) + + +@pytest.mark.django_db() +def test_user_login_basic_auth(api_client: test.APIClient): + user = factory.UserFactory() + login.user_login_basic_auth(api_client, user) + + +@pytest.mark.django_db() +def test_user_login_removes_expired_sessions(api_client: test.APIClient, faker: Faker): + login.user_login_removes_expired_sessions(api_client, faker) diff --git a/backend/tests/origin/integration/factory.py b/backend/tests/origin/integration/factory.py new file mode 100644 index 0000000..fe8a9fc --- /dev/null +++ b/backend/tests/origin/integration/factory.py @@ -0,0 +1,290 @@ +import contextlib +import datetime +import io +from typing import Any +from typing import ContextManager +from typing import List +from typing import Optional + +import django.contrib.auth.models +import django.utils.timezone +import factory +import faker.utils.decorators +from django.conf import settings +from django.db import models +from django.db.models import signals + +from eurydice.common import enums +from eurydice.common import minio +from eurydice.common.models import fields +from eurydice.origin.core import enums as origin_enums +from eurydice.origin.core import models as origin_models +from eurydice.origin.core import signals as eurydice_signals +from tests import utils + +POSITIVE_SMALL_INTEGER_FIELD_MAX_VALUE: int = 32767 + + +class UserFactoryWithSignal(factory.django.DjangoModelFactory): + """UserFactory with a signal for creating an associated UserProfile""" + + username = factory.Sequence(lambda n: f"{utils.fake.user_name()}_{n}") + first_name = factory.Faker("first_name") + last_name = factory.Faker("last_name") + password = factory.Faker("password") + is_superuser = False + + class Meta: + model = django.contrib.auth.get_user_model() + django_get_or_create = ("username",) + + +class UserFactory(UserFactoryWithSignal): + """UserFactory without a signal for creating an associated UserProfile""" + + @classmethod + def _create(cls, *args, **kwargs) -> django.contrib.auth.get_user_model(): + """Overridden _create method to disable the create_user_profile signal""" + signals.post_save.disconnect( + receiver=eurydice_signals.create_user_profile, sender=origin_models.User + ) + try: + instance = super()._create(*args, **kwargs) + finally: + signals.post_save.connect( + eurydice_signals.create_user_profile, sender=origin_models.User + ) + return instance + + +class UserProfileFactory(factory.django.DjangoModelFactory): + user = factory.SubFactory(UserFactory) + priority = factory.Faker( + "pyint", min_value=0, max_value=POSITIVE_SMALL_INTEGER_FIELD_MAX_VALUE + ) + + class Meta: + model = origin_models.UserProfile + + +class OutgoingTransferableFactory(factory.django.DjangoModelFactory): + name = factory.Faker("file_name") + size = factory.Faker("pyint", min_value=0, max_value=settings.TRANSFERABLE_MAX_SIZE) + user_profile = factory.SubFactory(UserProfileFactory) + user_provided_meta = {"Metadata-Foo": "Bar"} + + _sha1 = factory.Faker("sha1", raw_output=True) + _submission_succeeded = factory.Faker("pybool") + _submission_succeeded_at = factory.Faker( + "date_time_this_decade", tzinfo=django.utils.timezone.get_current_timezone() + ) + + @factory.lazy_attribute + def sha1(self) -> Optional[bytes]: + if self.submission_succeeded_at: + return self._sha1 + + return None + + @factory.lazy_attribute + def bytes_received(self) -> int: + if self.submission_succeeded_at: + return self.size + + return factory.Faker("pyint", min_value=0, max_value=self.size).evaluate( + None, None, {"locale": None} + ) + + @factory.lazy_attribute + def submission_succeeded_at(self) -> Optional[datetime.datetime]: + if self._submission_succeeded: + return self._submission_succeeded_at + + return None + + @factory.post_generation + def make_transferable_ranges_for_state( + self, + create: bool, + state: origin_enums.TransferableRangeTransferState, + **kwargs, + ): + if not create: + return + + if state == enums.OutgoingTransferableState.ONGOING: + TransferableRangeFactory( + transfer_state=origin_enums.TransferableRangeTransferState.TRANSFERRED, + outgoing_transferable=self, + **kwargs, + ) + TransferableRangeFactory( + transfer_state=origin_enums.TransferableRangeTransferState.PENDING, + outgoing_transferable=self, + **kwargs, + ) + elif state == enums.OutgoingTransferableState.CANCELED: + TransferableRevocationFactory( + reason=enums.TransferableRevocationReason.USER_CANCELED, + outgoing_transferable=self, + **kwargs, + ) + elif state == enums.OutgoingTransferableState.ERROR: + TransferableRevocationFactory( + reason=enums.TransferableRevocationReason.UNEXPECTED_EXCEPTION, + outgoing_transferable=self, + **kwargs, + ) + elif state == enums.OutgoingTransferableState.SUCCESS: + TransferableRangeFactory( + transfer_state=origin_enums.TransferableRangeTransferState.TRANSFERRED, + outgoing_transferable=self, + **kwargs, + ) + + class Meta: + model = origin_models.OutgoingTransferable + exclude = ("_sha1", "_submission_succeeded", "_submission_succeeded_at") + + +class TransferableRangeFactory(factory.django.DjangoModelFactory): + byte_offset = factory.Faker( + "pyint", min_value=0, max_value=settings.TRANSFERABLE_MAX_SIZE + ) + size = factory.Faker( + "pyint", min_value=0, max_value=settings.TRANSFERABLE_RANGE_SIZE + ) + transfer_state = factory.Faker( + "random_element", elements=origin_enums.TransferableRangeTransferState + ) + finished_at = factory.Faker( + "future_datetime", tzinfo=django.utils.timezone.get_current_timezone() + ) + + _s3_bucket_name = factory.Faker( + "pystr", + min_chars=fields.S3BucketNameField.MIN_LENGTH, + max_chars=fields.S3BucketNameField.MAX_LENGTH, + ) + _s3_object_name = factory.Faker( + "pystr", + min_chars=fields.S3ObjectNameField.MIN_LENGTH, + max_chars=fields.S3ObjectNameField.MAX_LENGTH, + ) + + @factory.lazy_attribute + @faker.utils.decorators.slugify + def s3_bucket_name(self) -> str: + return self._s3_bucket_name + + @factory.lazy_attribute + @faker.utils.decorators.slugify + def s3_object_name(self) -> str: + return self._s3_object_name + + @classmethod + def _create(cls, model_class: models.Model, *args, **kwargs) -> Any: + if ( + kwargs["transfer_state"] + == origin_enums.TransferableRangeTransferState.PENDING + ): + kwargs["finished_at"] = None + return super()._create(model_class, *args, **kwargs) + + target_transfer_state = kwargs["transfer_state"] + kwargs["transfer_state"] = origin_enums.TransferableRangeTransferState.PENDING + target_finished_at = kwargs["finished_at"] + kwargs["finished_at"] = None + + obj = super()._create(model_class, *args, **kwargs) + obj.transfer_state = target_transfer_state + obj.finished_at = target_finished_at + obj.save() + + return obj + + outgoing_transferable = factory.SubFactory(OutgoingTransferableFactory) + + class Meta: + model = origin_models.TransferableRange + exclude = ( + "_s3_bucket_name", + "_s3_object_name", + ) + + +class TransferableRevocationFactory(factory.django.DjangoModelFactory): + outgoing_transferable = factory.SubFactory(OutgoingTransferableFactory) + + reason = factory.Faker( + "random_element", elements=enums.TransferableRevocationReason + ) + + transfer_state = factory.Faker( + "random_element", elements=origin_enums.TransferableRevocationTransferState + ) + + class Meta: + model = origin_models.TransferableRevocation + + +@contextlib.contextmanager +def s3_stored_transferable_range( + data: bytes, **kwargs +) -> ContextManager[origin_models.TransferableRange]: + obj = TransferableRangeFactory(**kwargs) + + try: + minio.client.make_bucket(bucket_name=obj.s3_bucket_name) + minio.client.put_object( + bucket_name=obj.s3_bucket_name, + object_name=obj.s3_object_name, + data=io.BytesIO(data), + length=len(data), + ) + + yield obj + finally: + minio.client.remove_object( + bucket_name=obj.s3_bucket_name, object_name=obj.s3_object_name + ) + minio.client.remove_bucket(bucket_name=obj.s3_bucket_name) + + +@contextlib.contextmanager +def s3_stored_transferable_ranges( + data: bytes, count: int, s3_bucket_name: str, **kwargs +) -> ContextManager[List[origin_models.TransferableRange]]: + kwargs["s3_bucket_name"] = s3_bucket_name + + transferable_ranges = [TransferableRangeFactory(**kwargs) for _ in range(count)] + + try: + minio.client.make_bucket(bucket_name=s3_bucket_name) + for transferable_range in transferable_ranges: + minio.client.put_object( + bucket_name=s3_bucket_name, + object_name=transferable_range.s3_object_name, + data=io.BytesIO(data), + length=len(data), + ) + + yield transferable_ranges + finally: + for transferable_range in transferable_ranges: + minio.client.remove_object( + bucket_name=s3_bucket_name, + object_name=transferable_range.s3_object_name, + ) + minio.client.remove_bucket(bucket_name=s3_bucket_name) + + +__all__ = ( + "UserFactory", + "UserFactoryWithSignal", + "OutgoingTransferableFactory", + "TransferableRangeFactory", + "TransferableRevocationFactory", + "s3_stored_transferable_range", + "s3_stored_transferable_ranges", +) diff --git a/backend/tests/origin/integration/sender/__init__.py b/backend/tests/origin/integration/sender/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/integration/sender/packet_generator/__init__.py b/backend/tests/origin/integration/sender/packet_generator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/integration/sender/packet_generator/fillers/__init__.py b/backend/tests/origin/integration/sender/packet_generator/fillers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/integration/sender/packet_generator/fillers/test_transferable_range.py b/backend/tests/origin/integration/sender/packet_generator/fillers/test_transferable_range.py new file mode 100644 index 0000000..66c9404 --- /dev/null +++ b/backend/tests/origin/integration/sender/packet_generator/fillers/test_transferable_range.py @@ -0,0 +1,655 @@ +import datetime +import hashlib +from typing import List +from unittest import mock + +import factory +import freezegun +import pytest +from django.conf import Settings +from django.utils import timezone +from faker import Faker + +from eurydice.common import enums +from eurydice.common import exceptions +from eurydice.common import minio +from eurydice.common import protocol +from eurydice.origin.core import enums as origin_enums +from eurydice.origin.core import models +from eurydice.origin.sender.packet_generator.fillers import ( + transferable_range as transferable_range_filler, +) +from eurydice.origin.sender.user_selector import WeightedRoundRobinUserSelector +from tests.origin.integration import factory as origin_factory +from tests.origin.integration.utils import s3 + + +@pytest.mark.django_db() +def test__build_protocol_transferable_expect_size_and_sha1(): + now = timezone.now() - datetime.timedelta(seconds=2) + + with freezegun.freeze_time(now): + finish_date = now + datetime.timedelta(seconds=2) + transferable = origin_factory.OutgoingTransferableFactory( + submission_succeeded_at=finish_date, + ) + + origin_factory.TransferableRangeFactory( + transfer_state=origin_enums.TransferableRangeTransferState.TRANSFERRED, + outgoing_transferable=transferable, + finished_at=finish_date, + ) + + # query in order to have state annotated + queried_transferable = models.OutgoingTransferable.objects.prefetch_related( + "transferable_ranges" + ).get(id=transferable.id) + protocol_transferable = transferable_range_filler._build_protocol_transferable( + queried_transferable.transferable_ranges.select_related( + "outgoing_transferable" + ).last() + ) + + assert protocol_transferable.sha1 == bytes(transferable.sha1) + + +@pytest.mark.django_db() +def test__build_protocol_transferable(faker: Faker): + transferable = origin_factory.OutgoingTransferableFactory( + submission_succeeded_at=faker.date_time_this_decade( + tzinfo=timezone.get_current_timezone() + ) + ) + model = origin_factory.TransferableRangeFactory(outgoing_transferable=transferable) + + protocol_model = transferable_range_filler._build_protocol_transferable(model) + + assert model.outgoing_transferable.id == protocol_model.id + assert model.outgoing_transferable.name == protocol_model.name + assert model.outgoing_transferable.user_profile.id == protocol_model.user_profile_id + assert ( + model.outgoing_transferable.user_provided_meta + == protocol_model.user_provided_meta + ) + assert bytes(model.outgoing_transferable.sha1) == protocol_model.sha1 + assert model.outgoing_transferable.size == protocol_model.size + + +@pytest.mark.django_db() +def test__build_protocol_transferable_no_sha1(): + model = origin_factory.TransferableRangeFactory( + outgoing_transferable=origin_factory.OutgoingTransferableFactory( + submission_succeeded_at=None, sha1=None + ) + ) + + protocol_model = transferable_range_filler._build_protocol_transferable(model) + + assert protocol_model.sha1 is None + + +@pytest.mark.django_db() +def test__delete_objects_from_s3__empty_list(faker: Faker) -> None: + transferable_range_filler._delete_objects_from_s3([]) + + +@pytest.mark.django_db() +def test__delete_objects_from_s3(faker: Faker) -> None: + data = faker.binary(length=10) + bucket_name = f"test-{faker.pystr().lower()}" + with ( + origin_factory.s3_stored_transferable_ranges( + data, + 2, + bucket_name, + ) as transferable_ranges_in_same_bucket, + origin_factory.s3_stored_transferable_range(data) as other_transferable_range, + ): + transferable_range_filler._delete_objects_from_s3( + transferable_ranges_in_same_bucket + [other_transferable_range], + ) + + for transferable_range in transferable_ranges_in_same_bucket + [ + other_transferable_range + ]: + assert not s3.object_exists( + bucket=transferable_range.s3_bucket_name, + key=transferable_range.s3_object_name, + ) + + +@pytest.mark.django_db() +def test__get_transferable_range_data_success(faker: Faker): + data = faker.binary(length=10) + with origin_factory.s3_stored_transferable_range(data) as transferable_range: + s3_data = transferable_range_filler._get_transferable_range_data( + transferable_range + ) + assert hashlib.sha1(s3_data).digest() == hashlib.sha1(data).digest() + + +@pytest.mark.django_db() +def test__get_transferable_range_data_missing(faker: Faker): + data = faker.binary(length=10) + with origin_factory.s3_stored_transferable_range(data) as transferable_range: + minio.client.remove_object( + bucket_name=transferable_range.s3_bucket_name, + object_name=transferable_range.s3_object_name, + ) + with pytest.raises(exceptions.S3ObjectNotFoundError): + transferable_range_filler._get_transferable_range_data(transferable_range) + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + "transfer_states", + [ + [ + origin_enums.TransferableRangeTransferState.TRANSFERRED, + origin_enums.TransferableRangeTransferState.PENDING, + origin_enums.TransferableRangeTransferState.PENDING, + origin_enums.TransferableRangeTransferState.TRANSFERRED, + origin_enums.TransferableRangeTransferState.PENDING, + ], + [ + origin_enums.TransferableRangeTransferState.TRANSFERRED, + origin_enums.TransferableRangeTransferState.ERROR, + origin_enums.TransferableRangeTransferState.CANCELED, + ], + ], +) +def test__fetch_next_transferable_ranges( + transfer_states: List[origin_enums.TransferableRangeTransferState], +): + # add some noise + origin_factory.TransferableRangeFactory.create_batch( + len(origin_enums.TransferableRangeTransferState) * 3, + transfer_state=factory.Iterator( + origin_enums.TransferableRangeTransferState.values + ), + ) + + # actual test + current_user_profile = origin_factory.UserProfileFactory(priority=0) + next_transferable_ranges = [] + + for transfer_state in transfer_states: + new_range = origin_factory.TransferableRangeFactory( + outgoing_transferable__user_profile=current_user_profile, + transfer_state=transfer_state, + ) + + if transfer_state == origin_enums.TransferableRangeTransferState.PENDING: + next_transferable_ranges.append(new_range) + + # add an erroneous sibling, to also test the annotations + origin_factory.TransferableRangeFactory( + outgoing_transferable=new_range.outgoing_transferable, + transfer_state=origin_enums.TransferableRangeTransferState.ERROR, + ) + + fetched = transferable_range_filler._fetch_next_transferable_ranges_for_user( + current_user_profile.user + ) + + if next_transferable_ranges: + assert len(fetched) == len(next_transferable_ranges) + for elm in fetched: + assert elm in next_transferable_ranges + assert elm.erroneous_outgoing_transferable_id is not None + assert ( + elm.erroneous_outgoing_transferable_id == elm.outgoing_transferable.id + ) + else: + assert len(fetched) == 0 + + +@pytest.mark.django_db() +class TestTransferableRangeFiller: + def test_fill_success(self, faker: Faker): + """ + Assert filler fills the packet with the correct transferable range + Also assert that the TransferableRange's status was updated correctly. + """ + filler = transferable_range_filler.UserRotatingTransferableRangeFiller() + packet = protocol.OnTheWirePacket() + + data = faker.binary(length=10) + with origin_factory.s3_stored_transferable_range( + data, transfer_state=origin_enums.TransferableRangeTransferState.PENDING + ) as transferable_range: + a_date = timezone.now() + with freezegun.freeze_time(a_date): + filler.fill(packet=packet) + + transferable_range.refresh_from_db() + + assert not s3.object_exists( + bucket=transferable_range.s3_bucket_name, + key=transferable_range.s3_object_name, + ) + + assert len(packet.transferable_ranges) == 1 + assert packet.transferable_ranges[0].data == data + + assert ( + transferable_range.transfer_state + == origin_enums.TransferableRangeTransferState.TRANSFERRED + ) + assert transferable_range.finished_at == a_date + + def test_fill_no_pending_transferable_range(self): + """ + Asserts `fill` returns an empty list when there are no PENDING + TransferableRanges + """ + + filler = transferable_range_filler.UserRotatingTransferableRangeFiller() + + packet = protocol.OnTheWirePacket() + + origin_factory.TransferableRangeFactory( + transfer_state=origin_enums.TransferableRangeTransferState.TRANSFERRED + ) + + transferable_range_filler.UserRotatingTransferableRangeFiller.fill( + filler, packet + ) + + assert packet.transferable_ranges == [] + + def test_fill_no_next_user(self): + """ + Asserts `fill` returns an empty list when there are no next users + """ + + filler = transferable_range_filler.UserRotatingTransferableRangeFiller() + packet = protocol.OnTheWirePacket() + + transferable_range_filler.UserRotatingTransferableRangeFiller.fill( + filler, packet + ) + + assert packet.transferable_ranges == [] + + @mock.patch( + "eurydice.origin.sender.packet_generator.fillers.transferable_range._delete_objects_from_s3" # noqa: E501 + ) + def test_fill_cancel_revoked_transferable_ranges( + self, mocked_delete_objects_from_s3: mock.Mock + ): + filler = transferable_range_filler.UserRotatingTransferableRangeFiller() + packet = protocol.OnTheWirePacket() + + outgoing_transferable = origin_factory.OutgoingTransferableFactory() + + transferable_range = origin_factory.TransferableRangeFactory( + transfer_state=origin_enums.TransferableRangeTransferState.PENDING, + outgoing_transferable=outgoing_transferable, + ) + + origin_factory.TransferableRevocationFactory( + reason=enums.TransferableRevocationReason.UPLOAD_SIZE_MISMATCH, + outgoing_transferable=outgoing_transferable, + ) + + outgoing_transferable = models.OutgoingTransferable.objects.get( + id=outgoing_transferable.id + ) + assert outgoing_transferable.state == enums.OutgoingTransferableState.ERROR + + filler.fill(packet) + + transferable_range.refresh_from_db() + assert ( + transferable_range.transfer_state + == origin_enums.TransferableRangeTransferState.CANCELED + ) + mocked_delete_objects_from_s3.assert_called_once() + + assert packet.transferable_ranges == [] + + @mock.patch( + "eurydice.origin.sender.packet_generator.fillers.transferable_range._get_transferable_range_data" # noqa: E501 + ) + @mock.patch( + "eurydice.origin.sender.packet_generator.fillers.transferable_range._delete_objects_from_s3" # noqa: E501 + ) + def test_transferable_range_filler_fill_packet_size_too_large( + self, + patched_delete_objects_from_s3: mock.MagicMock, + patched_next_tr_data: mock.MagicMock, + settings: Settings, + ): + """ + Asserts `fill` stops adding transferable_ranges after the configured + TRANSFERABLE_RANGE_SIZE is exceeded + """ + + # prepare mocked functions + first_user_ranges = origin_factory.TransferableRangeFactory.create_batch( + 2, + transfer_state=origin_enums.TransferableRangeTransferState.PENDING, + outgoing_transferable__user_profile=origin_factory.UserProfileFactory(), + ) + + second_user_range = origin_factory.TransferableRangeFactory( + transfer_state=origin_enums.TransferableRangeTransferState.PENDING + ) + + patched_next_tr_data.return_value = ( + b"It was a bright cold day in April, and the clocks were striking thirteen" + ) + + patched_delete_objects_from_s3.return_value = None + + # also mock the user selector + mocked_user_selector = mock.create_autospec(WeightedRoundRobinUserSelector) + mocked_user_selector.get_next_user.side_effect = [ + first_user_ranges[0].outgoing_transferable.user_profile.user, + second_user_range.outgoing_transferable.user_profile.user, + None, + RuntimeError("get_next_user called too many times"), + ] + + # actual test + settings.TRANSFERABLE_RANGE_SIZE = 0 + + filler = transferable_range_filler.UserRotatingTransferableRangeFiller() + filler._user_selector = mocked_user_selector + packet = protocol.OnTheWirePacket() + transferable_range_filler.UserRotatingTransferableRangeFiller.fill( + filler, packet + ) + + assert len(packet.transferable_ranges) == 1 + assert ( + packet.transferable_ranges[0].transferable.id + == first_user_ranges[0].outgoing_transferable.id + ) + patched_delete_objects_from_s3.assert_called_once() + mocked_user_selector.get_next_user.assert_called_once() + + def test_fill_missing_s3_object(self, faker: Faker): + filler = transferable_range_filler.UserRotatingTransferableRangeFiller() + packet = protocol.OnTheWirePacket() + + data = faker.binary(length=10) + with origin_factory.s3_stored_transferable_range( + data, transfer_state=origin_enums.TransferableRangeTransferState.PENDING + ) as transferable_range: + minio.client.remove_object( + bucket_name=transferable_range.s3_bucket_name, + object_name=transferable_range.s3_object_name, + ) + a_date = timezone.now() + with freezegun.freeze_time(a_date): + filler.fill(packet=packet) + + transferable_range.refresh_from_db() + + assert len(packet.transferable_ranges) == 0 + + assert ( + transferable_range.transfer_state + == origin_enums.TransferableRangeTransferState.ERROR + ) + + assert transferable_range.finished_at == a_date + + +@pytest.mark.django_db() +class TestFIFOTransferableRangeFiller: + def test_fill_success(self, faker: Faker): + """ + Assert filler fills the packet with the correct transferable range. + Also assert that the TransferableRange's status was updated correctly. + """ + filler = transferable_range_filler.FIFOTransferableRangeFiller() + packet = protocol.OnTheWirePacket() + + data = faker.binary(length=10) + with origin_factory.s3_stored_transferable_range( + data, transfer_state=origin_enums.TransferableRangeTransferState.PENDING + ) as transferable_range: + a_date = timezone.now() + with freezegun.freeze_time(a_date): + filler.fill(packet=packet) + + transferable_range.refresh_from_db() + + assert not s3.object_exists( + bucket=transferable_range.s3_bucket_name, + key=transferable_range.s3_object_name, + ) + + assert len(packet.transferable_ranges) == 1 + assert packet.transferable_ranges[0].data == data + + assert ( + transferable_range.transfer_state + == origin_enums.TransferableRangeTransferState.TRANSFERRED + ) + assert transferable_range.finished_at == a_date + + +@pytest.mark.django_db() +def test_fetch_next_transferable_ranges_for_user_only_returns_user_transferable_ranges( + faker: Faker, +): + """ + Assert _fetch_next_transferable_ranges_for_user only returns TransferableRanges + for the given user. + """ + + user_profile = origin_factory.UserProfileFactory() + another_user_profile = origin_factory.UserProfileFactory() + + a_date = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + + with freezegun.freeze_time(a_date): + expected_transferable_range = origin_factory.TransferableRangeFactory( + outgoing_transferable=origin_factory.OutgoingTransferableFactory( + user_profile=user_profile + ), + transfer_state=origin_enums.TransferableRangeTransferState.PENDING, + finished_at=None, + ) + + with freezegun.freeze_time(a_date - datetime.timedelta(seconds=1)): + origin_factory.TransferableRangeFactory( + outgoing_transferable=origin_factory.OutgoingTransferableFactory( + user_profile=another_user_profile + ), + transfer_state=origin_enums.TransferableRangeTransferState.PENDING, + finished_at=None, + ) + + assert ( + transferable_range_filler._fetch_next_transferable_ranges_for_user( + user_profile.user + ).first() + == expected_transferable_range + ) + + +@pytest.mark.django_db() +def test_fetch_next_transferable_ranges_for_user_returns_nothing(): + """ + Assert _fetch_next_transferable_ranges_for_user returns nothing when passed + user_profile without any associated TransferableRanges + """ + + user_profile = origin_factory.UserProfileFactory() + + assert ( + transferable_range_filler._fetch_next_transferable_ranges_for_user( + user_profile.user + ).count() + == 0 + ) + + +@pytest.mark.django_db() +def test_fetch_next_transferable_ranges_for_user_returns_oldest_transferable_range( + faker: Faker, +): + """ + Assert _fetch_next_transferable_ranges_for_user returns the oldest of two + TransferableRanges both belonging to the given user. + """ + + user_profile = origin_factory.UserProfileFactory() + + a_date = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + + with freezegun.freeze_time(a_date): + origin_factory.TransferableRangeFactory( + outgoing_transferable=origin_factory.OutgoingTransferableFactory( + user_profile=user_profile + ), + transfer_state=origin_enums.TransferableRangeTransferState.PENDING, + finished_at=None, + ) + + with freezegun.freeze_time(a_date - datetime.timedelta(seconds=1)): + expected_transferable_range = origin_factory.TransferableRangeFactory( + outgoing_transferable=origin_factory.OutgoingTransferableFactory( + user_profile=user_profile + ), + transfer_state=origin_enums.TransferableRangeTransferState.PENDING, + finished_at=None, + ) + + assert ( + transferable_range_filler._fetch_next_transferable_ranges_for_user( + user_profile.user + ).first() + == expected_transferable_range + ) + + +@pytest.mark.django_db() +def test_fetch_next_transferable_ranges_for_user_filters_finished_transferable_ranges( + faker: Faker, +): + """ + Assert _fetch_next_transferable_ranges_for_user filters out finished + TransferableRanges + """ + a_date = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + + with freezegun.freeze_time(a_date - datetime.timedelta(seconds=2)): + transferable_range = origin_factory.TransferableRangeFactory( + transfer_state=origin_enums.TransferableRangeTransferState.TRANSFERRED, + finished_at=a_date, + ) + + assert ( + transferable_range_filler._fetch_next_transferable_ranges_for_user( + transferable_range.outgoing_transferable.user_profile.user + ).count() + == 0 + ) + + +@pytest.mark.django_db() +def test_fetch_pending_transferable_ranges_returns_transferable_ranges_for_any_user( + faker: Faker, +): + """ + Assert _fetch_pending_transferable_ranges returns TransferableRanges for any user. + """ + + user_profile = origin_factory.UserProfileFactory() + another_user_profile = origin_factory.UserProfileFactory() + + a_date = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + + with freezegun.freeze_time(a_date): + expected_transferable_range_user_1 = origin_factory.TransferableRangeFactory( + outgoing_transferable=origin_factory.OutgoingTransferableFactory( + user_profile=user_profile + ), + transfer_state=origin_enums.TransferableRangeTransferState.PENDING, + finished_at=None, + ) + + with freezegun.freeze_time(a_date - datetime.timedelta(seconds=1)): + expected_transferable_range_user_2 = origin_factory.TransferableRangeFactory( + outgoing_transferable=origin_factory.OutgoingTransferableFactory( + user_profile=another_user_profile + ), + transfer_state=origin_enums.TransferableRangeTransferState.PENDING, + finished_at=None, + ) + + assert list(transferable_range_filler._fetch_pending_transferable_ranges()) == [ + expected_transferable_range_user_2, + expected_transferable_range_user_1, + ] + + +@pytest.mark.django_db() +def test_fetch_pending_transferable_ranges_returns_nothing(): + """ + Assert _fetch_pending_transferable_ranges returns nothing when passed + there are no pending TransferableRanges + """ + assert transferable_range_filler._fetch_pending_transferable_ranges().count() == 0 + + +@pytest.mark.django_db() +def test_fetch_pending_transferable_ranges_returns_oldest_transferable_range( + faker: Faker, +): + """ + Assert _fetch_pending_transferable_ranges returns the oldest of two + TransferableRanges. + """ + + user_profile = origin_factory.UserProfileFactory() + + a_date = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + + with freezegun.freeze_time(a_date): + origin_factory.TransferableRangeFactory( + outgoing_transferable=origin_factory.OutgoingTransferableFactory( + user_profile=user_profile + ), + transfer_state=origin_enums.TransferableRangeTransferState.PENDING, + finished_at=None, + ) + + with freezegun.freeze_time(a_date - datetime.timedelta(seconds=1)): + expected_transferable_range = origin_factory.TransferableRangeFactory( + outgoing_transferable=origin_factory.OutgoingTransferableFactory( + user_profile=user_profile + ), + transfer_state=origin_enums.TransferableRangeTransferState.PENDING, + finished_at=None, + ) + + assert ( + transferable_range_filler._fetch_pending_transferable_ranges().first() + == expected_transferable_range + ) + + +@pytest.mark.django_db() +def test_fetch_pending_transferable_ranges_filters_finished_transferable_ranges( + faker: Faker, +): + """ + Assert _fetch_pending_transferable_ranges filters out finished + TransferableRanges + """ + a_date = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + + with freezegun.freeze_time(a_date - datetime.timedelta(seconds=2)): + origin_factory.TransferableRangeFactory( + transfer_state=origin_enums.TransferableRangeTransferState.TRANSFERRED, + finished_at=a_date, + ) + + assert transferable_range_filler._fetch_pending_transferable_ranges().count() == 0 diff --git a/backend/tests/origin/integration/sender/packet_generator/fillers/test_transferable_revocation.py b/backend/tests/origin/integration/sender/packet_generator/fillers/test_transferable_revocation.py new file mode 100644 index 0000000..6b950d9 --- /dev/null +++ b/backend/tests/origin/integration/sender/packet_generator/fillers/test_transferable_revocation.py @@ -0,0 +1,41 @@ +import pytest + +import eurydice.common.protocol as protocol +import eurydice.origin.core.models as models +import eurydice.origin.sender.packet_generator.fillers as fillers +import tests.origin.integration.factory as factory +from eurydice.origin.core import enums + + +@pytest.mark.django_db() +def test_transferable_revocation_filler_fill_success(): + expected_transferable_revocation = factory.TransferableRevocationFactory( + transfer_state=enums.TransferableRangeTransferState.PENDING + ) + other_transferable_revocation = factory.TransferableRevocationFactory( + transfer_state=enums.TransferableRangeTransferState.TRANSFERRED + ) + + filler = fillers.TransferableRevocationFiller() + + packet = protocol.OnTheWirePacket() + + filler.fill(packet) + + assert len(packet.transferable_revocations) == 1 + assert ( + packet.transferable_revocations[0].transferable_id + == expected_transferable_revocation.outgoing_transferable.id + ) + assert ( + models.TransferableRevocation.objects.get( + id=expected_transferable_revocation.id + ).transfer_state + == enums.TransferableRangeTransferState.TRANSFERRED + ) + assert ( + models.TransferableRevocation.objects.get( + id=other_transferable_revocation.id + ).transfer_state + == enums.TransferableRangeTransferState.TRANSFERRED + ) diff --git a/backend/tests/origin/integration/sender/test_packet_sender.py b/backend/tests/origin/integration/sender/test_packet_sender.py new file mode 100644 index 0000000..d2b90b9 --- /dev/null +++ b/backend/tests/origin/integration/sender/test_packet_sender.py @@ -0,0 +1,99 @@ +import socketserver +from typing import Iterator +from typing import Type +from unittest import mock + +import django.conf +import pytest +from faker import Faker + +from eurydice.common import protocol +from eurydice.origin.sender import packet_sender + + +@pytest.fixture() +def RequestHandler() -> Type: # noqa: N802 + class _RequestHandler(socketserver.StreamRequestHandler): + received = [] + + def handle(self): + _RequestHandler.received.append(self.rfile.read()) + + return _RequestHandler + + +@pytest.fixture() +def server(RequestHandler: Type) -> Iterator[socketserver.TCPServer]: # noqa: N803 + with socketserver.TCPServer(("localhost", 0), RequestHandler) as s: + yield s + + +@pytest.fixture() +def sender( + server: socketserver.TCPServer, settings: django.conf.Settings +) -> packet_sender.PacketSender: + settings.LIDIS_HOST, settings.LIDIS_PORT = server.socket.getsockname() + return packet_sender.PacketSender() + + +def test_packet_sender_success( + sender: packet_sender.PacketSender, server: socketserver.TCPServer +): + packet = protocol.OnTheWirePacket() + + sender.start() + sender.send(packet) + server.handle_request() + sender.stop() + + assert len(server.RequestHandlerClass.received) == 1 + received_data = server.RequestHandlerClass.received[0] + assert received_data == packet.to_bytes() + assert not sender._sender_thread.is_alive() + assert sender._queue.empty() + + +def test_packet_sender_context_manager_success( + sender: packet_sender.PacketSender, server: socketserver.TCPServer +): + packet = protocol.OnTheWirePacket() + + with sender as s: + s.send(packet) + server.handle_request() + + assert len(server.RequestHandlerClass.received) == 1 + received_data = server.RequestHandlerClass.received[0] + assert received_data == packet.to_bytes() + assert not sender._sender_thread.is_alive() + assert sender._queue.empty() + + +def test_packet_sender_send_multiple_times_success( + sender: packet_sender.PacketSender, server: socketserver.TCPServer, faker: Faker +): + with sender as s: + for i in range(10): + packet = mock.create_autospec(protocol.OnTheWirePacket) + serialized_packet = faker.binary(faker.pyint(min_value=1, max_value=100)) + packet.to_bytes.return_value = serialized_packet + + s.send(packet) + server.handle_request() + + assert len(server.RequestHandlerClass.received) == i + 1 + assert server.RequestHandlerClass.received[i] == serialized_packet + + assert not sender._sender_thread.is_alive() + assert sender._queue.empty() + + +def test_packet_sender_error_thread_not_running(settings: django.conf.Settings): + settings.LIDIS_HOST, settings.LIDIS_PORT = "localhost", 1 + + sender = packet_sender.PacketSender() + sender.start() + sender.stop() + + with pytest.raises(packet_sender.SenderThreadNotRunningError): + sender.send(protocol.OnTheWirePacket()) diff --git a/backend/tests/origin/integration/sender/test_sender.py b/backend/tests/origin/integration/sender/test_sender.py new file mode 100644 index 0000000..a9d5219 --- /dev/null +++ b/backend/tests/origin/integration/sender/test_sender.py @@ -0,0 +1,38 @@ +import os +import subprocess +import sys + +import pytest +from django.conf import settings + +from eurydice.origin import sender + + +@pytest.mark.django_db() +def test_start_and_graceful_shutdown(): + with subprocess.Popen( + [sys.executable, "-m", sender.__name__], + cwd=os.path.dirname(settings.BASE_DIR), + stderr=subprocess.PIPE, + env={ + "DB_NAME": settings.DATABASES["default"]["NAME"], + "DB_USER": settings.DATABASES["default"]["USER"], + "DB_PASSWORD": settings.DATABASES["default"]["PASSWORD"], + "DB_HOST": settings.DATABASES["default"]["HOST"], + "DB_PORT": str(settings.DATABASES["default"]["PORT"]), + "MINIO_ENDPOINT": settings.MINIO_ENDPOINT, + "MINIO_ACCESS_KEY": settings.MINIO_ACCESS_KEY, + "MINIO_SECRET_KEY": settings.MINIO_SECRET_KEY, + "MINIO_BUCKET_NAME": settings.MINIO_BUCKET_NAME, + "TRANSFERABLE_STORAGE_DIR": settings.TRANSFERABLE_STORAGE_DIR, + "LIDIS_HOST": settings.LIDIS_HOST, + "LIDIS_PORT": str(settings.LIDIS_PORT), + "USER_ASSOCIATION_TOKEN_SECRET_KEY": settings.USER_ASSOCIATION_TOKEN_SECRET_KEY, # noqa: E501 + }, + ) as proc: + while b"Ready to send OnTheWirePackets" not in proc.stderr.readline(): + pass + + proc.terminate() + return_code = proc.wait() + assert return_code == 0 diff --git a/backend/tests/origin/integration/sender/test_transferable_history_creator.py b/backend/tests/origin/integration/sender/test_transferable_history_creator.py new file mode 100644 index 0000000..20b978d --- /dev/null +++ b/backend/tests/origin/integration/sender/test_transferable_history_creator.py @@ -0,0 +1,157 @@ +import datetime + +import freezegun +import humanfriendly +import pytest +from django.conf import Settings +from django.utils import timezone + +import tests.origin.integration.factory as factory +from eurydice.origin.core import enums +from eurydice.origin.core import models +from eurydice.origin.sender.transferable_history_creator import ( + TransferableHistoryCreator, +) + + +@pytest.fixture() +def pending_transferable(): + transferable = factory.OutgoingTransferableFactory( + user_provided_meta={"Metadata-Foo": "Bar"}, + ) + factory.TransferableRangeFactory( + transfer_state=enums.TransferableRangeTransferState.PENDING, + outgoing_transferable=transferable, + ) + return transferable + + +@pytest.fixture() +def ongoing_transferable(): + now = timezone.now() - datetime.timedelta(seconds=1) + + with freezegun.freeze_time(now): + transferable = factory.OutgoingTransferableFactory( + user_provided_meta={"Metadata-Foo": "Bar"}, + ) + factory.TransferableRangeFactory( + transfer_state=enums.TransferableRangeTransferState.TRANSFERRED, + outgoing_transferable=transferable, + finished_at=now + datetime.timedelta(seconds=1), + ) + factory.TransferableRangeFactory( + transfer_state=enums.TransferableRangeTransferState.PENDING, + outgoing_transferable=transferable, + finished_at=None, + ) + return transferable + + +@pytest.fixture() +def transferred_transferable(): + now = timezone.now() - datetime.timedelta(seconds=2) + + with freezegun.freeze_time(now): + finish_date = now + datetime.timedelta(seconds=2) + transferable = factory.OutgoingTransferableFactory( + user_provided_meta={"Metadata-Foo": "Bar"}, + _submission_succeeded=True, + _submission_succeeded_at=finish_date, + ) + + factory.TransferableRangeFactory( + transfer_state=enums.TransferableRangeTransferState.TRANSFERRED, + outgoing_transferable=transferable, + finished_at=finish_date, + ) + return transferable + + +@pytest.fixture() +def errored_transferable(): + now = timezone.now() - datetime.timedelta(seconds=2) + + with freezegun.freeze_time(now): + transferable = factory.OutgoingTransferableFactory( + user_provided_meta={"Metadata-Foo": "Bar"}, + ) + + factory.TransferableRangeFactory( + transfer_state=enums.TransferableRangeTransferState.ERROR, + outgoing_transferable=transferable, + ) + return transferable + + +def test_get_next_history_too_soon_returns_none(settings: Settings): + settings.TRANSFERABLE_HISTORY_SEND_EVERY = humanfriendly.parse_timespan("1min") + + now = timezone.now() + + with freezegun.freeze_time(now): + creator = TransferableHistoryCreator() + creator._previous_history_generated_at = now - datetime.timedelta(seconds=1) + assert creator.get_next_history() is None + + +@pytest.mark.django_db() +def test_get_next_history_no_returns_old_transferables( + settings: Settings, transferred_transferable: models.OutgoingTransferable +): + settings.TRANSFERABLE_HISTORY_DURATION = humanfriendly.parse_timespan("1h") + + now = transferred_transferable.submission_succeeded_at + datetime.timedelta(hours=2) + + with freezegun.freeze_time(now): + creator = TransferableHistoryCreator() + assert creator._previous_history_generated_at is None + history = creator.get_next_history() + + assert history.entries == [] + assert creator._previous_history_generated_at == now + + +@pytest.mark.django_db() +def test_get_next_history_no_returns_non_final_state_transferables( + settings: Settings, + ongoing_transferable: models.OutgoingTransferable, + pending_transferable: models.OutgoingTransferable, +): + settings.TRANSFERABLE_HISTORY_DURATION_IN_HOURS = humanfriendly.parse_timespan("1h") + + now = ongoing_transferable.created_at + datetime.timedelta(minutes=15) + + with freezegun.freeze_time(now): + creator = TransferableHistoryCreator() + assert creator._previous_history_generated_at is None + history = creator.get_next_history() + + assert history.entries == [] + assert creator._previous_history_generated_at == now + + +@pytest.mark.django_db() +def test_get_next_history_returns_final_state_transferables( + settings: Settings, + transferred_transferable: models.OutgoingTransferable, + errored_transferable: models.OutgoingTransferable, +): + settings.TRANSFERABLE_HISTORY_DURATION_IN_HOURS = humanfriendly.parse_timespan("1h") + + now = transferred_transferable.created_at + datetime.timedelta(minutes=15) + + with freezegun.freeze_time(now): + creator = TransferableHistoryCreator() + assert creator._previous_history_generated_at is None + history = creator.get_next_history() + + history_entry_ids = [entry.transferable_id for entry in history.entries] + + expected_transferable_ids = [transferred_transferable.id, errored_transferable.id] + + assert sorted(history_entry_ids) == sorted(expected_transferable_ids) + + assert creator._previous_history_generated_at == now + + for entry in history.entries: + assert entry.user_provided_meta == {"Metadata-Foo": "Bar"} diff --git a/backend/tests/origin/integration/sender/test_user_selector.py b/backend/tests/origin/integration/sender/test_user_selector.py new file mode 100644 index 0000000..09fdb9b --- /dev/null +++ b/backend/tests/origin/integration/sender/test_user_selector.py @@ -0,0 +1,160 @@ +from uuid import UUID + +import factory +import pytest + +import eurydice.origin.core.models as models +import eurydice.origin.sender.user_selector as user_selector +import tests.origin.integration.factory as origin_factory +from eurydice.origin.core import enums + + +@pytest.mark.django_db() +def test_weighted_round_robin_user_selector_defaults_to_user_with_pending_transferable_ranges(): # noqa: E501 + pending_transferable_range = origin_factory.TransferableRangeFactory( + transfer_state=enums.TransferableRangeTransferState.PENDING + ) + + origin_factory.TransferableRangeFactory( + transfer_state=enums.TransferableRangeTransferState.TRANSFERRED + ) + + selector = user_selector.WeightedRoundRobinUserSelector() + selector.start_round() + + assert ( + selector.get_next_user() + == pending_transferable_range.outgoing_transferable.user_profile.user + ) + + +@pytest.mark.django_db() +def test_weighted_round_robin_user_selector_defaults_to_first_user_with_pending_transferable_ranges(): # noqa: E501 + origin_factory.TransferableRangeFactory.create_batch( + size=2, transfer_state=enums.TransferableRangeTransferState.PENDING + ) + + users = models.User.objects.all().order_by("id") + + # delete first user's OutgoingTransferables + models.TransferableRange.objects.filter( + outgoing_transferable__user_profile__user__id=users[0].id + ).delete() + + selector = user_selector.WeightedRoundRobinUserSelector() + selector.start_round() + + assert selector.get_next_user() == users[1] + + +@pytest.mark.django_db() +def test_weighted_round_robin_user_selector_defaults_to_none(): + """ + Assert WRR user selector returns None if no transferable ranges are pending + """ + + for transfer_state in enums.TransferableRangeTransferState: + if transfer_state is not enums.TransferableRangeTransferState.PENDING: + origin_factory.TransferableRangeFactory(transfer_state=transfer_state) + + selector = user_selector.WeightedRoundRobinUserSelector() + selector.start_round() + + assert selector.get_next_user() is None + + +@pytest.mark.django_db() +def test_weighted_round_robin_user_selector_round_count_behavior(): + # create two users + user_profiles = origin_factory.UserProfileFactory.create_batch( + 2, + priority=factory.Iterator((2, 1)), + user__id=factory.Iterator( + [ + UUID("00005dcf-5135-4b12-900d-d3bc7ab745fb"), + UUID("ffff9af2-6a6c-414e-ba54-516268b3b113"), + ] + ), + ) + + # create pending ranges + origin_factory.TransferableRangeFactory.create_batch( + 2, + outgoing_transferable__user_profile=factory.Iterator(user_profiles), + transfer_state=enums.TransferableRangeTransferState.PENDING, + ) + + selector = user_selector.WeightedRoundRobinUserSelector() + assert selector._round_counter == 0 + + selector.start_round() + + assert selector.get_next_user() == user_profiles[0].user + assert selector._round_counter == 1 + + selector.start_round() + assert selector.get_next_user() == user_profiles[0].user + assert selector._round_counter == 2 + + selector.start_round() + assert selector.get_next_user() == user_profiles[1].user + assert selector._round_counter == 1 + + selector.start_round() + assert selector.get_next_user() == user_profiles[0].user + assert selector._round_counter == 1 + + assert selector.get_next_user() == user_profiles[1].user + assert selector._round_counter == 1 + + assert selector.get_next_user() is None + + +@pytest.mark.django_db() +def test_weighted_round_robin_user_selector_returns_user_with_smallest_uuid(): # noqa: E501 + origin_factory.TransferableRangeFactory.create_batch( + size=10, transfer_state=enums.TransferableRangeTransferState.PENDING + ) + + users = models.User.objects.all().order_by("id") + + # delete first user's OutgoingTransferables + models.TransferableRange.objects.filter( + outgoing_transferable__user_profile__user__id=users[0].id + ).delete() + + selector = user_selector.WeightedRoundRobinUserSelector() + selector.start_round() + + # simulate the fact that users[0] should be selected again if they had + # PENDING ranges + selector._current_user = users[0] + selector._round_counter = 0 + + # make sure it's not the case and that lowest UUID is selected + assert selector.get_next_user() == users[1] + + +@pytest.mark.django_db() +def test_weighted_round_robin_user_selector_returns_none(): + """ + Assert WRR user selector returns None if no transferable ranges are pending + and the sole user is currently selected + """ + + current_user_profile = origin_factory.UserProfileFactory(priority=1) + + for transfer_state in enums.TransferableRangeTransferState: + if transfer_state is not enums.TransferableRangeTransferState.PENDING: + origin_factory.TransferableRangeFactory( + outgoing_transferable__user_profile=current_user_profile, + transfer_state=transfer_state, + ) + + selector = user_selector.WeightedRoundRobinUserSelector() + selector.start_round() + + selector._current_user = current_user_profile.user + selector._round_counter = 0 + + assert selector.get_next_user() is None diff --git a/backend/tests/origin/integration/sender/test_utils.py b/backend/tests/origin/integration/sender/test_utils.py new file mode 100644 index 0000000..ed21a87 --- /dev/null +++ b/backend/tests/origin/integration/sender/test_utils.py @@ -0,0 +1,32 @@ +from typing import Optional + +import django.core.exceptions as django_exceptions +import pytest +from django.conf import Settings + +import eurydice.origin.sender.utils as sender_utils + + +@pytest.mark.parametrize( + ("lidis_host", "lidis_port", "expected_exception"), + [ + (None, None, django_exceptions.ImproperlyConfigured), + ("127.0.0.1", None, django_exceptions.ImproperlyConfigured), + (None, 666, django_exceptions.ImproperlyConfigured), + ("127.0.0.1", 666, None), + ], +) +def test_loop_exception_missing_lidis_address( + lidis_host: Optional[str], + lidis_port: Optional[int], + expected_exception: Optional[django_exceptions.ImproperlyConfigured], + settings: Settings, +): + settings.LIDIS_HOST = lidis_host + settings.LIDIS_PORT = lidis_port + + if expected_exception is None: + sender_utils.check_configuration() + else: + with pytest.raises(expected_exception): + sender_utils.check_configuration() diff --git a/backend/tests/origin/integration/utils/__init__.py b/backend/tests/origin/integration/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/integration/utils/s3.py b/backend/tests/origin/integration/utils/s3.py new file mode 100644 index 0000000..c7fa9f7 --- /dev/null +++ b/backend/tests/origin/integration/utils/s3.py @@ -0,0 +1,25 @@ +from minio.error import S3Error + +from eurydice.common import minio + + +def object_exists(bucket: str, key: str) -> bool: + result = True + response = None + try: + response = minio.client.get_object( + bucket_name=bucket, + object_name=key, + ) + except S3Error as e: + if e.code != "NoSuchKey": + raise + result = False + finally: + if response: + response.close() + response.release_conn() + return result + + +__all__ = ("object_exists",) diff --git a/backend/tests/origin/integration/utils/test_orm.py b/backend/tests/origin/integration/utils/test_orm.py new file mode 100644 index 0000000..ea6cd4d --- /dev/null +++ b/backend/tests/origin/integration/utils/test_orm.py @@ -0,0 +1,72 @@ +import factory +import pytest +from django.db.models import F +from django.db.models import Q + +from eurydice.common.utils import orm +from eurydice.origin.core import enums +from eurydice.origin.core import models +from tests.origin.integration import factory as origin_factory + + +@pytest.mark.django_db() +def test_outgoing_transferable_state(): + # Create a pending transferable range for an erroneous outgoing transferable + erroneous_outgoing_transferable = origin_factory.OutgoingTransferableFactory( + _submission_succeeded=True, + ) + + origin_factory.TransferableRangeFactory.create_batch( + 3, + transfer_state=factory.Iterator( + ( + enums.TransferableRangeTransferState.TRANSFERRED, + enums.TransferableRangeTransferState.ERROR, + enums.TransferableRangeTransferState.PENDING, + ) + ), + outgoing_transferable=erroneous_outgoing_transferable, + ) + + # Create a basic pending transferable range + pending_outgoing_transferable = origin_factory.OutgoingTransferableFactory( + _submission_succeeded=True, + ) + + origin_factory.TransferableRangeFactory( + transfer_state=enums.TransferableRangeTransferState.PENDING, + outgoing_transferable=pending_outgoing_transferable, + ) + + # Black magic + queryset = orm.make_queryset_with_subquery_join( + models.TransferableRange.objects.only("id").filter( + transfer_state=enums.TransferableRangeTransferState.PENDING + ), + models.TransferableRange.objects.values("outgoing_transferable_id").filter( + transfer_state=enums.TransferableRangeTransferState.ERROR + ), + on=Q(outgoing_transferable_id=F("outgoing_transferable_id")), + select={"erroneous_outgoing_transferable_id": "outgoing_transferable_id"}, + ) + + # Checks + assert len(queryset) == 2 + + found_pending = False + found_erroneous = False + + for transferable_range in queryset: + if transferable_range.outgoing_transferable == pending_outgoing_transferable: + found_pending = True + assert transferable_range.erroneous_outgoing_transferable_id is None + + if transferable_range.outgoing_transferable == erroneous_outgoing_transferable: + found_erroneous = True + assert ( + transferable_range.erroneous_outgoing_transferable_id + == erroneous_outgoing_transferable.id + ) + + assert found_erroneous + assert found_pending diff --git a/backend/tests/origin/unit/__init__.py b/backend/tests/origin/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/unit/api/__init__.py b/backend/tests/origin/unit/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/unit/api/utils/__init__.py b/backend/tests/origin/unit/api/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/unit/api/utils/test_metadata_headers.py b/backend/tests/origin/unit/api/utils/test_metadata_headers.py new file mode 100644 index 0000000..64a27e3 --- /dev/null +++ b/backend/tests/origin/unit/api/utils/test_metadata_headers.py @@ -0,0 +1,31 @@ +from typing import Dict + +import pytest + +from eurydice.origin.api.utils import metadata_headers + + +@pytest.mark.parametrize( + ("headers", "expected"), + [ + # Test expected header + ({"Metadata-Name": "OSS117"}, {"Metadata-Name": "OSS117"}), + # Test one incorrect header + ({"X-Metadata-Name": "OSS117"}, {}), + # Test one incorrect header with a correct one + ( + { + "Choose-Life": "Choose a job. Choose a career...", + "Metadata-agent": "007", + }, + {"Metadata-agent": "007"}, + ), + ], +) +def test_extract_metadata_from_headers( + headers: Dict[str, str], expected: Dict[str, str] +): + assert ( + metadata_headers.extract_metadata_from_headers(headers) # type: ignore + == expected + ) diff --git a/backend/tests/origin/unit/api/utils/test_partitioned_stream.py b/backend/tests/origin/unit/api/utils/test_partitioned_stream.py new file mode 100644 index 0000000..6a46942 --- /dev/null +++ b/backend/tests/origin/unit/api/utils/test_partitioned_stream.py @@ -0,0 +1,125 @@ +import hashlib +import io + +import pytest +from faker import Faker + +from eurydice.origin.api.utils import PartitionedStream +from tests import utils + +TEST_PARAMS = [ + # test when stream_partition_size == len(bytes) + ( + bytes("reality must take precedence over public relations", "utf-8"), + 50, + ), + # test when stream_partition_size > len(bytes) + ( + bytes("reality must take precedence over public relations", "utf-8"), + utils.fake.pyint(min_value=51), + ), + # test when stream_partition_size < len(bytes) + ( + bytes("reality must take precedence over public relations", "utf-8"), + utils.fake.pyint(min_value=1, max_value=49), + ), + # test random bytes with random stream_partition_size + ( + utils.fake.binary(utils.fake.pyint(min_value=1, max_value=20)), + utils.fake.pyint(), + ), +] + + +@pytest.mark.parametrize(("random_bytes", "stream_partition_size"), TEST_PARAMS) +def test_partitioned_stream_full(random_bytes: bytes, stream_partition_size: int): + """ + Assert all bytes inputted are read correctly + """ + + stream = io.BytesIO(random_bytes) + + all_read_bytes = b"" + + for partition in PartitionedStream(stream, stream_partition_size): + all_read_bytes += partition.read() + + assert all_read_bytes == random_bytes + + +def test_partitioned_stream_digest_update(faker: Faker): + """ + Assert the Stream's digest is computed correctly accross multiple SubStreams + """ + random_bytes = faker.binary(faker.pyint(min_value=2, max_value=20)) + + # random stream part size + # capped to ensure at least 2 partitioned_streams are used + stream_partition_size = faker.pyint(max_value=len(random_bytes) - 1) + + stream = io.BytesIO(random_bytes) + + stream_digest = hashlib.sha1() + + for partition in PartitionedStream( + stream, stream_partition_size, stream_digest.update + ): + partition.read() + + assert stream_digest.hexdigest() == hashlib.sha1(random_bytes).hexdigest() + + +@pytest.mark.parametrize(("random_bytes", "stream_partition_size"), TEST_PARAMS) +def test_partitioned_stream_default_chunk_size( + random_bytes: bytes, stream_partition_size: int +): + """ + Assert .read() method's chunk_size argument defaults to + the correct value (ie: stream_partition_size) + """ + stream = io.BytesIO(random_bytes) + + stream_partition = next(iter(PartitionedStream(stream, stream_partition_size))) + + if stream_partition_size > len(random_bytes): + assert len(stream_partition.read()) == len(random_bytes) + else: + assert len(stream_partition.read()) == stream_partition_size + + +def test_partitioned_stream_chunk_size(faker: Faker): + """ + Assert .read() method's chunk_size argument returns the correct number of bytes + """ + random_bytes = b"Les requetes trop rapides" + stream = io.BytesIO(random_bytes) + + stream_partition = next(iter(PartitionedStream(stream, len(random_bytes)))) + + assert len(stream_partition.read(0)) == 0 + + read_chunk_size = faker.pyint(max_value=len(random_bytes) - 1) + + bytes_read = stream_partition.read(read_chunk_size) + + assert len(bytes_read) == read_chunk_size + + bytes_read += stream_partition.read() + + assert bytes_read == random_bytes + + +def test_partitioned_stream__partition_eof(): + """ + Assert partitioned_stream._partition_eof is toggled when `partition_size` bytes + have been read + """ + random_bytes = bytes("Les requetes trop rapides", "utf-8") + stream = io.BytesIO(random_bytes) + + stream_partition = next(iter(PartitionedStream(stream, len(random_bytes)))) + read_chunk_size = len(random_bytes) + + assert stream_partition._partition_eof is False + assert stream_partition.read(read_chunk_size) == random_bytes + assert stream_partition._partition_eof is True diff --git a/backend/tests/origin/unit/api/views/test_outgoing_transferable.py b/backend/tests/origin/unit/api/views/test_outgoing_transferable.py new file mode 100644 index 0000000..72c2161 --- /dev/null +++ b/backend/tests/origin/unit/api/views/test_outgoing_transferable.py @@ -0,0 +1,61 @@ +import logging +from typing import Optional +from unittest import mock + +import pytest +from django.conf import Settings + +from eurydice.origin.api import exceptions +from eurydice.origin.api.views import outgoing_transferable + + +def test__get_content_length_transfer_encoding_chunked( + caplog: pytest.LogCaptureFixture, +): + caplog.set_level(logging.WARNING) + + request = mock.Mock() + request.headers = {"Transfer-Encoding": "chunked", "Content-Length": "1"} + + assert outgoing_transferable._get_content_length(request) is None + assert caplog.messages == [ + "Cannot retrieve the 'Content-Length' header of an HTTP request " + "using chunked transfer encoding." + ] + + +@pytest.mark.parametrize( + ("content_length_header", "transferable_max_size", "expected_result"), + [ + (None, 10, None), + ("1", 2, 1), + ( + "2", + 1, + exceptions.RequestEntityTooLargeError, + ), + ("foo", 1, exceptions.InvalidContentLengthError), + ("-5", 1, None), + ], +) +def test__get_content_length( + content_length_header: str, + transferable_max_size: int, + expected_result: Optional[Exception], + settings: Settings, +): + request = mock.Mock() + if content_length_header: + request.headers = {"Content-Length": content_length_header} + else: + request.headers = {} + + settings.TRANSFERABLE_MAX_SIZE = transferable_max_size + + if isinstance(expected_result, type(Exception)): + with pytest.raises(expected_result): + outgoing_transferable._get_content_length(request) + elif expected_result is None: + assert outgoing_transferable._get_content_length(request) is None + else: + assert outgoing_transferable._get_content_length(request) == expected_result diff --git a/backend/tests/origin/unit/core/__init__.py b/backend/tests/origin/unit/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/unit/core/models/__init__.py b/backend/tests/origin/unit/core/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/unit/core/models/test_maintenance.py b/backend/tests/origin/unit/core/models/test_maintenance.py new file mode 100644 index 0000000..303c932 --- /dev/null +++ b/backend/tests/origin/unit/core/models/test_maintenance.py @@ -0,0 +1,10 @@ +import pytest +from django.db.utils import IntegrityError + +from eurydice.origin.core.models.maintenance import Maintenance + + +@pytest.mark.django_db() +def test_cant_create_other_maintenance_instance() -> None: + with pytest.raises(IntegrityError): + Maintenance.objects.create(maintenance=True) diff --git a/backend/tests/origin/unit/core/models/test_outgoing_transferable.py b/backend/tests/origin/unit/core/models/test_outgoing_transferable.py new file mode 100644 index 0000000..db7a1f3 --- /dev/null +++ b/backend/tests/origin/unit/core/models/test_outgoing_transferable.py @@ -0,0 +1,23 @@ +from unittest import mock + +import faker +import freezegun +from django.db import models +from django.utils import timezone + +from eurydice.origin.core.models import outgoing_transferable + + +def test_PythonNow(faker: faker.Faker): # noqa: N802 + now = outgoing_transferable.PythonNow() + assert now.value is None + + fake_datetime = faker.date_time_this_decade(tzinfo=timezone.get_current_timezone()) + with freezegun.freeze_time(fake_datetime), mock.patch.object( + models.Value, + "as_sql", + return_value="", + ) as patched_now_as_sql: + now.as_sql() + patched_now_as_sql.assert_called_once() + assert now.value == fake_datetime diff --git a/backend/tests/origin/unit/core/test_triggers.py b/backend/tests/origin/unit/core/test_triggers.py new file mode 100644 index 0000000..c17aad0 --- /dev/null +++ b/backend/tests/origin/unit/core/test_triggers.py @@ -0,0 +1,242 @@ +import datetime + +import factory as factory_boy +import freezegun +import pytest +from django.db import transaction +from django.db.utils import InternalError +from django.utils import timezone +from faker import Faker + +from eurydice.common import enums as common_enums +from eurydice.origin.core import enums +from eurydice.origin.core import models +from tests.origin.integration import factory + + +@pytest.mark.django_db() +def test_range_auto_fields_standard_usage(): + transferable = factory.OutgoingTransferableFactory() + noise_transferable = factory.OutgoingTransferableFactory() + + assert transferable.auto_revocations_count == 0 + assert transferable.auto_user_revocations_count == 0 + assert transferable.auto_ranges_count == 0 + assert transferable.auto_pending_ranges_count == 0 + assert transferable.auto_transferred_ranges_count == 0 + assert transferable.auto_canceled_ranges_count == 0 + assert transferable.auto_error_ranges_count == 0 + assert transferable.auto_last_range_finished_at is None + assert transferable.auto_bytes_transferred == 0 + + bytes_transferred = 0 + + for current_transferable in (transferable, noise_transferable): + for num_ranges, state in enumerate(enums.TransferableRangeTransferState): + for _ in range(num_ranges + 1): + transferable_range = factory.TransferableRangeFactory( + transfer_state=state, + outgoing_transferable=current_transferable, + ) + + if current_transferable == transferable: + last_range_finished_at = transferable_range.finished_at + if ( + transferable_range.transfer_state + == enums.TransferableRangeTransferState.TRANSFERRED + ): + bytes_transferred += transferable_range.size + + transferable.refresh_from_db() + + assert transferable.auto_revocations_count == 0 + assert transferable.auto_user_revocations_count == 0 + assert transferable.auto_ranges_count == 10 + assert transferable.auto_pending_ranges_count == 1 + assert transferable.auto_transferred_ranges_count == 2 + assert transferable.auto_canceled_ranges_count == 3 + assert transferable.auto_error_ranges_count == 4 + assert transferable.auto_last_range_finished_at == last_range_finished_at + assert transferable.auto_bytes_transferred == bytes_transferred + + +@pytest.mark.django_db() +def test_revocation_auto_fields_standard_usage(): + ( + transferable, + user_transferable, + noise_transferable, + ) = factory.OutgoingTransferableFactory.create_batch(3) + + assert transferable.auto_revocations_count == 0 + assert transferable.auto_user_revocations_count == 0 + + factory.TransferableRangeFactory( + transfer_state=enums.TransferableRangeTransferState.CANCELED, + outgoing_transferable=user_transferable, + ) + + factory.TransferableRevocationFactory.create_batch( + 3, + reason=factory_boy.Iterator( + ( + common_enums.TransferableRevocationReason.UPLOAD_SIZE_MISMATCH, + common_enums.TransferableRevocationReason.USER_CANCELED, + common_enums.TransferableRevocationReason.USER_CANCELED, + ) + ), + outgoing_transferable=factory_boy.Iterator( + (transferable, user_transferable, noise_transferable) + ), + ) + + transferable.refresh_from_db() + user_transferable.refresh_from_db() + noise_transferable.refresh_from_db() + + assert transferable.auto_revocations_count == 1 + assert transferable.auto_user_revocations_count == 0 + assert transferable.auto_ranges_count == 0 + assert transferable.auto_pending_ranges_count == 0 + assert transferable.auto_transferred_ranges_count == 0 + assert transferable.auto_canceled_ranges_count == 0 + assert transferable.auto_error_ranges_count == 0 + assert transferable.auto_last_range_finished_at is None + assert transferable.auto_bytes_transferred == 0 + + assert user_transferable.auto_revocations_count == 1 + assert user_transferable.auto_user_revocations_count == 1 + + +@pytest.mark.django_db() +@pytest.mark.parametrize( + "field", + [ + "auto_revocations_count", + "auto_user_revocations_count", + "auto_ranges_count", + "auto_pending_ranges_count", + "auto_transferred_ranges_count", + "auto_canceled_ranges_count", + "auto_error_ranges_count", + "auto_last_range_finished_at", + "auto_bytes_transferred", + "auto_state_updated_at", + ], +) +def test_auto_fields_protection(field: str): + transferable = factory.OutgoingTransferableFactory() + + # the atomic transaction is necessary, otherwise `django_db` thinks the + # test failed (because the test transaction is broken) + with pytest.raises(InternalError), transaction.atomic(): + transferable.save(update_fields=[field]) + + +@pytest.mark.django_db() +def test_ranges_forbidden_inserts(faker: Faker): + transferable = factory.OutgoingTransferableFactory() + + # the atomic transaction is necessary, otherwise `django_db` thinks the + # test failed (because the test transaction is broken) + with pytest.raises(InternalError), transaction.atomic(): + models.TransferableRange.objects.create( + byte_offset=0, + size=218, + s3_bucket_name="postgres <3", + s3_object_name="potatoe", + transfer_state=enums.TransferableRangeTransferState.TRANSFERRED, + finished_at=faker.future_datetime(tzinfo=timezone.get_current_timezone()), + outgoing_transferable=transferable, + ) + + +@pytest.mark.django_db() +def test_ranges_forbidden_updates(): + transferable_range = factory.TransferableRangeFactory( + transfer_state=enums.TransferableRangeTransferState.PENDING + ) + + transferable = factory.OutgoingTransferableFactory() + + # the atomic transaction is necessary, otherwise `django_db` thinks the + # test failed (because the test transaction is broken) + with pytest.raises(InternalError), transaction.atomic(): + transferable_range.mark_as_transferred(save=False) + transferable_range.outgoing_transferable = transferable + transferable_range.save() + + with pytest.raises(InternalError), transaction.atomic(): + transferable_range.size = 5 + transferable_range.save() + + transferable_range.mark_as_transferred() + + with pytest.raises(InternalError), transaction.atomic(): + transferable_range.mark_as_canceled() + + +@pytest.mark.django_db() +def test_revocations_forbidden_updates(): + transferable_revocation = factory.TransferableRevocationFactory( + reason=common_enums.TransferableRevocationReason.UNEXPECTED_EXCEPTION, + transfer_state=enums.TransferableRevocationTransferState.PENDING, + ) + + # the atomic transaction is necessary, otherwise `django_db` thinks the + # test failed (because the test transaction is broken) + with pytest.raises(InternalError), transaction.atomic(): + transferable_revocation.reason = ( + common_enums.TransferableRevocationReason.USER_CANCELED + ) + transferable_revocation.save() + + transferable_revocation.transfer_state = ( + enums.TransferableRevocationTransferState.TRANSFERRED + ) + transferable_revocation.save(update_fields=["transfer_state"]) + + +@pytest.mark.django_db() +def test_state_update_through_ranges(): + transferable = factory.OutgoingTransferableFactory() + + current_time = transferable.auto_state_updated_at + assert current_time is not None + + for state in enums.TransferableRangeTransferState: + current_time = current_time + datetime.timedelta(minutes=1) + with freezegun.freeze_time(current_time): + factory.TransferableRangeFactory( + transfer_state=state, + outgoing_transferable=transferable, + finished_at=( + None + if state == enums.TransferableRangeTransferState.PENDING + else current_time + ), + ) + + transferable.refresh_from_db() + assert transferable.auto_state_updated_at == current_time + + +@pytest.mark.django_db() +@pytest.mark.parametrize("reason", common_enums.TransferableRevocationReason) +def test_state_update_through_revocation( + reason: common_enums.TransferableRevocationReason, +): + transferable = factory.OutgoingTransferableFactory() + + current_time = transferable.auto_state_updated_at + assert current_time is not None + + current_time = current_time + datetime.timedelta(minutes=1) + with freezegun.freeze_time(current_time): + factory.TransferableRevocationFactory( + reason=reason, + outgoing_transferable=transferable, + ) + + transferable.refresh_from_db() + assert transferable.auto_state_updated_at == current_time diff --git a/backend/tests/origin/unit/sender/__init__.py b/backend/tests/origin/unit/sender/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/unit/sender/packet_generator/__init__.py b/backend/tests/origin/unit/sender/packet_generator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/unit/sender/packet_generator/fillers/__init__.py b/backend/tests/origin/unit/sender/packet_generator/fillers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/origin/unit/sender/packet_generator/fillers/test_history.py b/backend/tests/origin/unit/sender/packet_generator/fillers/test_history.py new file mode 100644 index 0000000..0f2c3c6 --- /dev/null +++ b/backend/tests/origin/unit/sender/packet_generator/fillers/test_history.py @@ -0,0 +1,18 @@ +from unittest import mock + +from eurydice.common import protocol +from eurydice.origin.sender.packet_generator import fillers + + +def test_ongoing_history_filler_fill_success(): + filler = mock.Mock() + + expected_history = protocol.History(entries=[]) + filler._history_creator.get_next_history.return_value = expected_history + packet = protocol.OnTheWirePacket() + + assert packet.history is None + + fillers.OngoingHistoryFiller.fill(filler, packet) + + assert packet.history is expected_history diff --git a/backend/tests/origin/unit/sender/packet_generator/fillers/test_transferable_range.py b/backend/tests/origin/unit/sender/packet_generator/fillers/test_transferable_range.py new file mode 100644 index 0000000..0aa9293 --- /dev/null +++ b/backend/tests/origin/unit/sender/packet_generator/fillers/test_transferable_range.py @@ -0,0 +1,25 @@ +from unittest import mock + +import pytest + +import eurydice.common.protocol as protocol +from eurydice.origin.sender.packet_generator.fillers import ( + transferable_range as transferable_range_filler, +) + + +def test_transferable_range_filler__fill_already_exists_transferable_ranges_in_packet(): # noqa: E501 + """ + Assert `fill` method raises exception if passed a Packet which already contains + TransferableRanges + """ + packet = protocol.OnTheWirePacket() + + packet.transferable_ranges.append(1) + + mocked_filler = mock.Mock() + + with pytest.raises(transferable_range_filler.OTWPacketAlreadyHasTransferableRanges): + transferable_range_filler.UserRotatingTransferableRangeFiller.fill( + mocked_filler, packet + ) diff --git a/backend/tests/origin/unit/sender/packet_generator/test_generator.py b/backend/tests/origin/unit/sender/packet_generator/test_generator.py new file mode 100644 index 0000000..1d12c3b --- /dev/null +++ b/backend/tests/origin/unit/sender/packet_generator/test_generator.py @@ -0,0 +1,25 @@ +from unittest import mock + +import pytest + +import eurydice.origin.sender.packet_generator.fillers as fillers +import eurydice.origin.sender.packet_generator.generator as generator + + +@pytest.mark.django_db() +@mock.patch.object(fillers.UserRotatingTransferableRangeFiller, "fill") +@mock.patch.object(fillers.TransferableRevocationFiller, "fill") +@mock.patch.object(fillers.OngoingHistoryFiller, "fill") +def test_generator_generate_next_packet( + fill_transferable_ranges: fillers.UserRotatingTransferableRangeFiller, + fill_revocations: fillers.TransferableRevocationFiller, + fill_history: fillers.OngoingHistoryFiller, +): + packet_generator = generator.OnTheWirePacketGenerator() + fill_transferable_ranges.return_value = 1 + + packet = packet_generator.generate_next_packet() + + fill_transferable_ranges.assert_called_once_with(packet) + fill_revocations.assert_called_once_with(packet) + fill_history.assert_called_once_with(packet) diff --git a/backend/tests/origin/unit/sender/test_main.py b/backend/tests/origin/unit/sender/test_main.py new file mode 100644 index 0000000..fa1d67e --- /dev/null +++ b/backend/tests/origin/unit/sender/test_main.py @@ -0,0 +1,108 @@ +import datetime +import logging +from unittest import mock + +import pytest +from django.conf import Settings +from django.utils import timezone + +from eurydice.common import protocol +from eurydice.common.utils import signals +from eurydice.origin.sender import main +from eurydice.origin.sender import packet_generator +from eurydice.origin.sender import packet_sender + + +@mock.patch.object(signals.BooleanCondition, "__bool__") +@mock.patch.object(packet_generator.OnTheWirePacketGenerator, "generate_next_packet") +@mock.patch.object(packet_sender.PacketSender, "send") +@mock.patch("eurydice.origin.sender.main._heartbeat_should_be_sent") +@pytest.mark.django_db() +def test__main_running( + patched__heartbeat_should_be_sent: mock.MagicMock, + patched_send: mock.MagicMock, + patched_generate_next_packet: mock.MagicMock, + patched_running_condition_bool: mock.MagicMock, + caplog: pytest.LogCaptureFixture, + settings: Settings, +): + caplog.set_level(logging.INFO) + packets = [ + # this packet should be sent because + # the first packet is always sent as heartbeat + protocol.OnTheWirePacket(), + # this packet should be sent because it is not empty + protocol.OnTheWirePacket(history=protocol.History(entries=[])), + # this packet should not be sent because it is empty + protocol.OnTheWirePacket(), + ] + patched_generate_next_packet.side_effect = packets + patched__heartbeat_should_be_sent.side_effect = [True, False, False] + patched_running_condition_bool.side_effect = [True, True, True, False] + + calls = [mock.call(packets[0]), mock.call(packets[1])] + + settings.LIDIS_HOST = "127.0.0.1" + settings.LIDIS_PORT = 666 + main._loop() + + patched_send.assert_has_calls(calls) + assert caplog.messages[0] == "Ready to send OnTheWirePackets" + assert caplog.messages[1] == "Sending heartbeat" + assert caplog.messages[2] == ( + "Sending OnTheWirePacket" + ) + assert len(caplog.messages) == 3 + + +@mock.patch.object(signals.BooleanCondition, "__bool__") +@mock.patch.object(packet_generator.OnTheWirePacketGenerator, "generate_next_packet") +def test_main_not_running( + patched_generate_next_packet: mock.MagicMock, + patched_running_bool: mock.MagicMock, + settings: Settings, +): + settings.LIDIS_HOST = "127.0.0.1" + settings.LIDIS_PORT = 666 + patched_running_bool.return_value = False + main._loop() + assert not patched_generate_next_packet.called + + +def test__heartbeat_should_be_sent_no_last_packet(): + assert main._heartbeat_should_be_sent(None) is True + + +@pytest.mark.parametrize( + ("time_delta_in_seconds", "expected_return"), [(5, False), (10, True), (11, True)] +) +def test__heartbeat_should_be_sent( + time_delta_in_seconds: int, expected_return: bool, settings: Settings +): + settings.HEARTBEAT_SEND_EVERY = 10 + + last_packet_sent_at = timezone.now() - datetime.timedelta( + seconds=time_delta_in_seconds + ) + + assert main._heartbeat_should_be_sent(last_packet_sent_at) is expected_return + + +def test___log_packet_stats(caplog: pytest.LogCaptureFixture): + caplog.set_level(logging.INFO) + packet = protocol.OnTheWirePacket(history=protocol.History(entries=[])) + main._log_packet_stats(packet) + + assert [ + "Sending OnTheWirePacket" + ] == caplog.messages + + +def test___log_packet_stats_empty_packet(caplog: pytest.LogCaptureFixture): + caplog.set_level(logging.INFO) + packet = protocol.OnTheWirePacket() + main._log_packet_stats(packet) + + assert ["Sending heartbeat"] == caplog.messages diff --git a/backend/tests/origin/unit/sender/test_packet_sender.py b/backend/tests/origin/unit/sender/test_packet_sender.py new file mode 100644 index 0000000..49c0887 --- /dev/null +++ b/backend/tests/origin/unit/sender/test_packet_sender.py @@ -0,0 +1,27 @@ +import queue + +import django.conf +import pytest + +from eurydice.origin.sender import packet_sender + + +class Test_SenderThread: # noqa: N801 + def test_run_stop_with_poison_pill(self): + qu = queue.Queue(maxsize=1) + thread = packet_sender._SenderThread(qu) + qu.put(None, block=False) + thread.run() + assert qu.empty() + + def test_run_log_error( + self, settings: django.conf.Settings, caplog: pytest.LogCaptureFixture + ): + settings.LIDIS_HOST, settings.LIDIS_PORT = "localhost", 1 + qu = queue.Queue(maxsize=2) + thread = packet_sender._SenderThread(qu) + qu.put(b"Lorem ipsum dolor sit amet", block=False) + qu.put(None, block=False) + thread.run() + assert qu.empty() + assert "Failed to send data through the socket." in caplog.text diff --git a/backend/tests/utils.py b/backend/tests/utils.py new file mode 100644 index 0000000..eb161df --- /dev/null +++ b/backend/tests/utils.py @@ -0,0 +1,7 @@ +import faker +from django import conf + +fake = faker.Faker() +fake.seed_instance(conf.settings.FAKER_SEED) + +__all__ = ("fake",) diff --git a/compose.kibana.yml b/compose.kibana.yml new file mode 100644 index 0000000..dc62c3d --- /dev/null +++ b/compose.kibana.yml @@ -0,0 +1,108 @@ +x-filebeat-common: &filebeat-common + environment: + ELASTICSEARCH_API_KEY: ${ELASTICSEARCH_API_KEY} + ELASTICSEARCH_HOSTS: ${ELASTICSEARCH_HOSTS:-elasticsearch:9200} + image: elastic/filebeat:${ELK_VERSION:-7.17.11} + networks: + - eurydice + read_only: true + command: + - filebeat + - run + - --strict.perms=false + - -e + - -E + - setup.kibana.host="http://kibana:5601" + - output.console={pretty:true} + healthcheck: + test: + - CMD + - test + - output + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + depends_on: + elasticsearch: + condition: service_healthy + +services: + + filebeat-origin: + <<: *filebeat-common + volumes: + - "${FILEBEAT_CONFIG_PATH:-./filebeat/filebeat.origin.yml}:/usr/share/filebeat/filebeat.yml:ro" + - ${ELASTICSEARCH_CERT_PATH:-/etc/ssl/certs/ca-certificates.crt}:/usr/share/elasticsearch/config/certs/cert.crt:ro + - filebeat-origin-elastic-registry:/usr/share/filebeat/data + - db-origin-logs:/logs/origin/postgresql:ro + - backend-origin-logs:/logs/origin/backend:ro + - sender-logs:/logs/origin/sender:ro + - dbtrimmer-origin-logs:/logs/origin/dbtrimmer:ro + + filebeat-destination: + <<: *filebeat-common + volumes: + - "${FILEBEAT_CONFIG_PATH:-./filebeat/filebeat.destination.yml}:/usr/share/filebeat/filebeat.yml:ro" + - ${ELASTICSEARCH_CERT_PATH:-/etc/ssl/certs/ca-certificates.crt}:/usr/share/elasticsearch/config/certs/cert.crt:ro + - filebeat-destination-elastic-registry:/usr/share/filebeat/data + - db-destination-logs:/logs/destination/postgresql:ro + - backend-destination-logs:/logs/destination/backend:ro + - receiver-logs:/logs/destination/receiver:ro + - dbtrimmer-destination-logs:/logs/destination/dbtrimmer:ro + - s3remover-logs:/logs/destination/s3remover:ro + + elasticsearch: + image: elasticsearch:${ELK_VERSION:-7.17.11} + networks: + - eurydice + environment: + discovery.type: single-node + ES_JAVA_OPTS: -Xms512m -Xmx512m + volumes: + - elastic:/usr/share/elasticsearch/data" + healthcheck: + test: + [ + "CMD-SHELL", + "curl --silent --fail localhost:9200/_cluster/health?wait_for_status=yellow&timeout=30s; wait $$! || exit 1", + ] + interval: 5s + timeout: 40s + retries: 5 + start_period: 30s + labels: + traefik.enable: "true" + traefik.http.routers.elasticsearch.rule: Host(`elasticsearch.localhost`) + + kibana: + image: kibana:${ELK_VERSION:-7.17.11} + networks: + - eurydice + environment: + SERVER_NAME: kibana.localhost + SERVER_HOST: "0.0.0.0" + SERVER_PUBLICBASEURL: http://kibana.localhost + TELEMETRY_ENABLED: "false" + ELASTICSEARCH_URL: http://elasticsearch:9200 + depends_on: + elasticsearch: + condition: service_healthy + healthcheck: + test: + [ + "CMD-SHELL", + "curl --fail --silent localhost:5601/api/status || exit 1", + ] + interval: 5s + timeout: 5s + retries: 5 + start_period: 45s + labels: + traefik.enable: "true" + traefik.http.routers.kibana.rule: Host(`kibana.localhost`) + +volumes: + elastic: + filebeat-origin-elastic-registry: + filebeat-destination-elastic-registry: diff --git a/compose.prod.yml b/compose.prod.yml new file mode 100644 index 0000000..3d81c0c --- /dev/null +++ b/compose.prod.yml @@ -0,0 +1,585 @@ +x-backend-common-envs: &backend-common-envs + DB_HOST: db + DB_NAME: eurydice + DB_USER: eurydice + DB_PASSWORD: ${DB_PASSWORD} + DB_PORT: "5432" + MINIO_ENDPOINT: ${MINIO_ENDPOINT:-minio:9000} + MINIO_BUCKET_NAME: eurydice + MINIO_ACCESS_KEY: minio + MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} + MINIO_ENABLED: ${MINIO_ENABLED:-true} + USER_ASSOCIATION_TOKEN_SECRET_KEY: ${USER_ASSOCIATION_TOKEN_SECRET_KEY} + GUNICORN_CONFIGURATION: ${GUNICORN_CONFIGURATION} + EURYDICE_CONTACT: ${EURYDICE_CONTACT:-the development team} + EURYDICE_CONTACT_FR: ${EURYDICE_CONTACT_FR:-} + UI_BADGE_CONTENT: ${UI_BADGE_CONTENT:-} + UI_BADGE_COLOR: ${UI_BADGE_COLOR:-brown} + LOG_TO_FILE: ${LOG_TO_FILE:-true} + LOG_LEVEL: ${LOG_LEVEL:-INFO} + +x-backend-common: &backend-common + image: ${LOCAL_DOCKER_REGISTRY:-docker.io}/anssi/eurydice-backend:${EURYDICE_VERSION:-latest} + user: ${UID:-1000}:${GID:-1000} + networks: + - eurydice + read_only: true + restart: always + tmpfs: + - /tmp + depends_on: + db: + condition: service_started + minio: + condition: service_healthy + +x-backend: &backend + <<: *backend-common + cpus: ${CPUS_BACKEND:-4} + mem_limit: ${MEM_LIMIT_BACKEND:-8GB} + ports: + - "${HTTP_SERVICES_BIND_HOST:-127.0.0.1}:8080:8080" + restart: always + +x-dbtrimmer: &dbtrimmer + <<: *backend-common + cpus: ${CPUS_DBTRIMMER:-2} + mem_limit: ${MEM_LIMIT_DBTRIMMER:-2GB} + environment: + <<: *backend-common-envs + DBTRIMMER_TRIM_TRANSFERABLES_AFTER: ${DBTRIMMER_TRIM_TRANSFERABLES_AFTER:-7days} + DBTRIMMER_RUN_EVERY: ${DBTRIMMER_RUN_EVERY:-6h} + DBTRIMMER_POLL_EVERY: ${DBTRIMMER_POLL_EVERY:-200ms} + restart: always + +x-frontend: &frontend + image: ${LOCAL_DOCKER_REGISTRY:-docker.io}/anssi/eurydice-frontend:${EURYDICE_VERSION:-latest} + user: ${UID:-1000}:${GID:-1000} + cpus: ${CPUS_FRONTEND:-4} + mem_limit: ${MEM_LIMIT_FRONTEND:-500M} + ports: + - "${HTTP_SERVICES_BIND_HOST:-127.0.0.1}:8888:8080" + networks: + - eurydice + read_only: true + tmpfs: + - /tmp:uid=${UID:-1000},gid=${GID:-1000},mode=700 + - /var/cache/nginx:uid=${UID:-1000},gid=${GID:-1000},mode=700 + - /etc/nginx/conf.d:uid=${UID:-1000},gid=${GID:-1000},mode=700 + restart: always + +x-filebeat: &filebeat + image: ${REMOTE_DOCKER_REGISTRY:-docker.io}/elastic/filebeat:${ELK_VERSION:-7.17.11} + user: ${UID:-1000}:${GID:-1000} + cpus: ${CPUS_FILEBEAT:-2} + mem_limit: ${MEM_LIMIT_FILEBEAT:-500m} + read_only: true + networks: + - eurydice + restart: always + environment: + ELASTICSEARCH_API_KEY: + ELASTICSEARCH_HOSTS: + command: + - filebeat + - run + - --strict.perms=false + - -e + healthcheck: + test: + - CMD + - test + - output + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + +services: + ############# Common services ############# + + minio: + image: ${REMOTE_DOCKER_REGISTRY:-docker.io}/minio/minio:RELEASE.2023-08-23T10-07-06Z + user: ${UID:-1000}:${GID:-1000} + cpus: ${CPUS_MINIO:-4} + mem_limit: ${MEM_LIMIT_MINIO:-4GB} + ports: + - ${HTTP_SERVICES_BIND_HOST:-127.0.0.1}:9001:9001 + networks: + - eurydice + environment: + MINIO_ROOT_USER: minio + MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} + read_only: true + restart: always + volumes: + - ${MINIO_DATA_DIR}:/data + - ${MINIO_CONF_DIR}:/config + command: + - server + - /data + - --config-dir + - /config/ + - --certs-dir + - /config/.certs/ + - --console-address + - :9001 + healthcheck: + test: + - CMD-SHELL + - curl http://localhost:9001/minio/health/live + interval: 5s + timeout: 5s + retries: 10 + start_period: 2s + + db: + image: ${REMOTE_DOCKER_REGISTRY:-docker.io}/postgres:15 + read_only: true + shm_size: ${SHM_SIZE_DATABASE:-1g} + cpus: ${CPUS_DATABASE:-4} + mem_limit: ${MEM_LIMIT_DATABASE:-8GB} + networks: + - eurydice + restart: always + environment: + POSTGRES_DB: eurydice + POSTGRES_USER: eurydice + POSTGRES_PASSWORD: ${DB_PASSWORD} + user: ${UID:-1000}:${GID:-1000} + tmpfs: + - /tmp:mode=770,uid=${UID:-1000},gid=${GID:-1000} + - /run:mode=770,uid=${UID:-1000},gid=${GID:-1000} + volumes: + - ${DB_DATA_DIR}:/var/lib/postgresql/data + - ${DB_LOGS_DIR}:/var/log/postgresql + command: + - "postgres" + - "-c" # turn on csv logging + - "logging_collector=on" + - "-c" + - "log_destination=csvlog" + - "-c" + - "log_directory=/var/log/postgresql" + - "-c" # filebeat format + - "log_line_prefix=%m [%p] %q%u@%d " + - "-c" # turn on hour-based log rotation + - "log_truncate_on_rotation=on" + - "-c" + - "log_filename=postgresql-%H.log" + - "-c" + - "log_rotation_age=60" + - "-c" + - "log_file_mode=0644" + - "-c" # print statements that took more than 200ms... + - "log_min_duration_statement=200" + - "-c" # ...do not print other statements... + - "log_statement=none" + - "-c" # ...do not even print the duration of these other statements + - "log_duration=off" + healthcheck: + test: + - CMD-SHELL + - psql -U eurydice -lqtA | grep -q "^eurydice|" + interval: 5s + timeout: 5s + retries: 5 + ports: + - "${HTTP_SERVICES_BIND_HOST:-127.0.0.1}:5050:80" + + ############# Origin services ############# + frontend-origin: + <<: *frontend + profiles: + - origin + - origin-with-elk-logging + environment: + NGINX_SERVER_NAME: ${FRONTEND_HOSTNAMES:-localhost} + NGINX_ROOT_DIR: origin + + filebeat-origin: + <<: *filebeat + profiles: + - origin-with-elk-logging + volumes: + - "${FILEBEAT_CONFIG_PATH}:/usr/share/filebeat/filebeat.yml:ro" + - "${FILEBEAT_LOGS_DIR}:/usr/share/filebeat/logs" + - "${FILEBEAT_DATA_DIR}:/usr/share/filebeat/data" + - "${ELASTICSEARCH_CERT_PATH}:/usr/share/elasticsearch/config/certs/cert.crt:ro" + - "${DB_LOGS_DIR}:/logs/origin/postgresql:ro" + - "${PYTHON_LOGS_DIR}/backend-origin:/logs/origin/backend:ro" + - "${PYTHON_LOGS_DIR}/sender:/logs/origin/sender:ro" + - "${PYTHON_LOGS_DIR}/dbtrimmer-origin:/logs/origin/dbtrimmer:ro" + - "${PYTHON_LOGS_DIR}/db-migrations-origin:/logs/origin/db-migrations:ro" + + backend-origin: &backend-origin + <<: *backend + profiles: + - origin + volumes: + - "${PYTHON_LOGS_DIR}/backend-origin:/var/log/app" + environment: + <<: *backend-common-envs + SECRET_KEY: ${DJANGO_SECRET_KEY} + ALLOWED_HOSTS: ${BACKEND_HOSTNAMES:-localhost} + CSRF_TRUSTED_ORIGINS: ${CSRF_TRUSTED_ORIGINS} + EURYDICE_API: origin + USER_ASSOCIATION_TOKEN_EXPIRES_AFTER: ${USER_ASSOCIATION_TOKEN_EXPIRES_AFTER:-30min} + REMOTE_USER_HEADER_AUTHENTICATION_ENABLED: + REMOTE_USER_HEADER: + METRICS_SLIDING_WINDOW: ${METRICS_SLIDING_WINDOW:-60min} + TRANSFERABLE_MAX_SIZE: ${EURYDICE_TRANSFERABLE_MAX_SIZE:-54975581388800} + command: + - make + - run-origin-api + healthcheck: + test: + - CMD + - /healthcheck.py + interval: 5s + timeout: 5s + retries: 5 + start_period: 1s + + backend-origin-with-elk: + <<: *backend-origin + profiles: + - origin-with-elk-logging + depends_on: + filebeat-origin: + condition: service_healthy + + dbtrimmer-origin: &dbtrimmer-origin + <<: *dbtrimmer + volumes: + - "${PYTHON_LOGS_DIR}/dbtrimmer-origin:/var/log/app" + profiles: + - origin + command: + - make + - run-origin-dbtrimmer + + dbtrimmer-origin-with-elk: + <<: *dbtrimmer-origin + profiles: + - origin-with-elk-logging + depends_on: + filebeat-origin: + condition: service_healthy + + sender: &sender + <<: *backend-common + profiles: + - origin + cpus: ${CPUS_SENDER:-2} + mem_limit: ${MEM_LIMIT_SENDER:-6GB} + volumes: + - "${PYTHON_LOGS_DIR}/sender:/var/log/app" + extra_hosts: + - "lidis.docker.host:${LIDIS_DOCKER_HOST:-host-gateway}" + environment: + <<: *backend-common-envs + LIDIS_HOST: lidis.docker.host + LIDIS_PORT: "33010" + TRANSFERABLE_HISTORY_DURATION: ${TRANSFERABLE_HISTORY_DURATION} + TRANSFERABLE_HISTORY_SEND_EVERY: ${TRANSFERABLE_HISTORY_SEND_EVERY} + SENDER_RANGE_FILLER_CLASS: ${SENDER_RANGE_FILLER_CLASS:-UserRotatingTransferableRangeFiller} + restart: always + depends_on: + db: + condition: service_healthy + minio: + condition: service_healthy + lidis: + condition: service_started + command: + - make + - run-sender + + sender-with-elk: + <<: *sender + profiles: + - origin-with-elk-logging + depends_on: + db: + condition: service_healthy + minio: + condition: service_healthy + lidis: + condition: service_started + filebeat-origin: + condition: service_healthy + + lidis: + profiles: + - origin + - origin-with-elk-logging + image: ${LOCAL_DOCKER_REGISTRY:-docker.io}/eurydice/lidi-send:${LIDI_VERSION:-latest} + cpus: ${CPUS_LIDIS:-4} + mem_limit: ${MEM_LIMIT_LIDIS:-4GB} + read_only: true + network_mode: "host" + environment: + RUST_LOG: ${RUST_LOG:-INFO} + restart: always + command: + - "--from_tcp" + - "${LIDIS_DOCKER_HOST:-0.0.0.0}:33010" + - "--to_udp" + - "${LIDIR_HOST}:${LIDIR_PORT:-11011}" + - "--to_udp_mtu" + - "${LIDI_UDP_MTU:-1500}" + - "--nb_clients" + - "1" + - "--encoding_block_size" + - "${LIDI_ENCODING_BLOCK_SIZE:-60000}" + - "--repair_block_size" + - "${LIDI_REPAIR_BLOCK_SIZE:-6000}" + - "--udp_buffer_size" + - "${LIDIS_UDP_BUFFER_SIZE:-32768}" + + ############# Origin tools ############# + db-migrations-origin: &db-migrations-origin + profiles: + - migration-origin + <<: *backend + volumes: + - "${PYTHON_LOGS_DIR}/db-migrations-origin:/var/log/app" + environment: + <<: *backend-common-envs + SECRET_KEY: ${DJANGO_SECRET_KEY} + ALLOWED_HOSTS: ${BACKEND_HOSTNAMES:-localhost} + EURYDICE_API: origin + command: + - make + - migrate + depends_on: + db: + condition: service_healthy + minio: + condition: service_healthy + + db-migrations-origin-with-elk: + profiles: + - migration-origin-with-elk-logging + <<: *db-migrations-origin + depends_on: + db: + condition: service_healthy + minio: + condition: service_healthy + filebeat-origin: + condition: service_healthy + + ############# Destination services ############# + lidir: &lidir + profiles: + - destination + image: ${LOCAL_DOCKER_REGISTRY:-docker.io}/eurydice/lidi-receive:${LIDI_VERSION:-latest} + depends_on: + receiver: + condition: service_started + cpus: ${CPUS_LIDIR:-4} + mem_limit: ${MEM_LIMIT_LIDIR:-4GB} + read_only: true + network_mode: "host" + environment: + RUST_LOG: ${RUST_LOG:-INFO} + restart: always + command: + - "--from_udp" + - "${LIDIR_HOST:-0.0.0.0}:${LIDIR_PORT:-11011}" + - "--from_udp_mtu" + - "${LIDI_UDP_MTU:-1500}" + - "--to_tcp" + - "127.0.0.1:65432" + - "--encoding_block_size" + - "${LIDI_ENCODING_BLOCK_SIZE:-60000}" + - "--repair_block_size" + - "${LIDI_REPAIR_BLOCK_SIZE:-6000}" + - "--nb_clients" + - "1" + + lidir-with-elk: + <<: *lidir + profiles: + - destination-with-elk-logging + depends_on: + receiver-with-elk: + condition: service_started + + receiver: &receiver + profiles: + - destination + <<: *backend-common + volumes: + - "${PYTHON_LOGS_DIR}/receiver:/var/log/app" + - "${TRANSFERABLE_STORAGE_DIR}:/home/eurydice/data" + cpus: ${CPUS_RECEIVER:-2} + mem_limit: ${MEM_LIMIT_RECEIVER:-6GB} + environment: + <<: *backend-common-envs + PACKET_RECEIVER_HOST: "0.0.0.0" + PACKET_RECEIVER_PORT: "65432" + RECEIVER_BUFFER_MAX_ITEMS: "${RECEIVER_BUFFER_MAX_ITEMS:-6}" + restart: always + ports: + - "127.0.0.1:65432:65432" + command: + - make + - run-receiver + + receiver-with-elk: + profiles: + - destination-with-elk-logging + <<: *receiver + depends_on: + filebeat-destination: + condition: service_healthy + + backend-destination: &backend-destination + profiles: + - destination + <<: *backend + volumes: + - "${PYTHON_LOGS_DIR}/backend-destination:/var/log/app" + - "${TRANSFERABLE_STORAGE_DIR}:/home/eurydice/data" + environment: + <<: *backend-common-envs + SECRET_KEY: ${DJANGO_SECRET_KEY} + ALLOWED_HOSTS: ${BACKEND_HOSTNAMES:-localhost} + CSRF_TRUSTED_ORIGINS: ${CSRF_TRUSTED_ORIGINS} + EURYDICE_API: destination + REMOTE_USER_HEADER_AUTHENTICATION_ENABLED: + REMOTE_USER_HEADER: + METRICS_SLIDING_WINDOW: ${METRICS_SLIDING_WINDOW:-60min} + command: + - make + - run-destination-api + healthcheck: + test: + - CMD + - /healthcheck.py + interval: 5s + timeout: 5s + retries: 5 + start_period: 1s + + backend-destination-with-elk: + profiles: + - destination-with-elk-logging + <<: *backend-destination + depends_on: + filebeat-destination: + condition: service_healthy + + frontend-destination: + <<: *frontend + profiles: + - destination + - destination-with-elk-logging + environment: + NGINX_SERVER_NAME: ${FRONTEND_HOSTNAMES:-localhost} + NGINX_ROOT_DIR: destination + + filebeat-destination: + <<: *filebeat + profiles: + - destination-with-elk-logging + volumes: + - "${FILEBEAT_CONFIG_PATH}:/usr/share/filebeat/filebeat.yml:ro" + - "${FILEBEAT_LOGS_DIR}:/usr/share/filebeat/logs" + - "${FILEBEAT_DATA_DIR}:/usr/share/filebeat/data" + - "${ELASTICSEARCH_CERT_PATH}:/usr/share/elasticsearch/config/certs/cert.crt:ro" + - "${DB_LOGS_DIR}:/logs/destination/postgresql:ro" + - "${PYTHON_LOGS_DIR}/backend-destination:/logs/destination/backend:ro" + - "${PYTHON_LOGS_DIR}/receiver:/logs/destination/receiver:ro" + - "${PYTHON_LOGS_DIR}/dbtrimmer-destination:/logs/destination/dbtrimmer:ro" + - "${PYTHON_LOGS_DIR}/s3remover:/logs/destination/s3remover:ro" + - "${PYTHON_LOGS_DIR}/db-migrations-destination:/logs/destination/db-migrations:ro" + + dbtrimmer-destination: &dbtrimmer-destination + profiles: + - destination + <<: *dbtrimmer + volumes: + - "${PYTHON_LOGS_DIR}/dbtrimmer-destination:/var/log/app" + command: + - make + - run-destination-dbtrimmer + + dbtrimmer-destination-with-elk: + profiles: + - destination-with-elk-logging + <<: *dbtrimmer-destination + depends_on: + filebeat-destination: + condition: service_healthy + + s3remover: &s3remover + profiles: + - destination + <<: *backend-common + volumes: + - "${PYTHON_LOGS_DIR}/s3remover:/var/log/app" + - "${TRANSFERABLE_STORAGE_DIR}:/home/eurydice/data" + cpus: ${CPUS_S3REMOVER:-2} + mem_limit: ${MEM_LIMIT_S3REMOVER:-1GB} + environment: + <<: *backend-common-envs + S3REMOVER_EXPIRE_TRANSFERABLES_AFTER: + S3REMOVER_RUN_EVERY: + S3REMOVER_POLL_EVERY: + restart: always + command: + - make + - run-destination-s3remover + + s3remover-with-elk: + profiles: + - destination-with-elk-logging + <<: *s3remover + depends_on: + filebeat-destination: + condition: service_healthy + + ############# Destination tools ############# + db-migrations-destination: &db-migrations-destination + profiles: + - migration-destination + <<: *backend + volumes: + - "${PYTHON_LOGS_DIR}/db-migrations-destination:/var/log/app" + environment: + <<: *backend-common-envs + SECRET_KEY: ${DJANGO_SECRET_KEY} + ALLOWED_HOSTS: ${BACKEND_HOSTNAMES:-localhost} + EURYDICE_API: destination + command: + - make + - migrate + depends_on: + db: + condition: service_healthy + minio: + condition: service_healthy + + db-migrations-destination-with-elk: + profiles: + - migration-destination-with-elk-logging + <<: *db-migrations-destination + depends_on: + db: + condition: service_healthy + minio: + condition: service_healthy + filebeat-destination: + condition: service_healthy + +networks: + eurydice: + driver: bridge + name: eurydice + ipam: + config: + - subnet: ${EURYDICE_IPV4_SUBNET:-172.18.0.0/24} diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..116fdaf --- /dev/null +++ b/compose.yml @@ -0,0 +1,469 @@ +x-backend-common-envs: &backend-common-envs + DB_NAME: eurydice + DB_USER: eurydice + DB_PASSWORD: eurydice + DB_PORT: "5432" + DJANGO_ENV: DEV + DEBUG: "True" + DJANGO_MANAGEPY_MIGRATE: "off" + MINIO_ACCESS_KEY: minio + MINIO_SECRET_KEY: miniominio + TZ: "Europe/Paris" + SECURE_COOKIES: "false" + USER_ASSOCIATION_TOKEN_SECRET_KEY: "Baby shark for 36 bytes dudududududu" + EURYDICE_CONTACT: ${EURYDICE_CONTACT:-the development team} + EURYDICE_CONTACT_FR: ${EURYDICE_CONTACT_FR:-par courrier recommandé en 3 exemplaires} + UI_BADGE_CONTENT: Dev -> Dev + UI_BADGE_COLOR: "green" + LOG_TO_FILE: true + +x-backend: &backend + build: + context: "backend" + dockerfile: docker/Dockerfile + target: dev + # NOTE: we tag local dev images with our registry otherwise the image is tagged + # using the docker hub as a registry + # https://docs.docker.com/engine/reference/commandline/tag/#extended-description + image: eurydice/backend:dev + networks: + - eurydice + command: + - make + - run-dev + +x-common-origin-envs: &common-origin-envs + DB_HOST: db-origin + MINIO_ENABLED: true + MINIO_ENDPOINT: minio:9000 + MINIO_BUCKET_NAME: eurydice-origin + # Maximum object size per operation allowed by minio (50 TiB) + # https://min.io/docs/minio/linux/operations/checklists/thresholds.html#minio-server-limits + TRANSFERABLE_MAX_SIZE: 54975581388800 + +x-common-destination-envs: &common-destination-envs + DB_HOST: db-destination + MINIO_ENABLED: false + TRANSFERABLE_STORAGE_DIR: /tmp/eurydice-data/ + MINIO_BUCKET_NAME: eurydice-destination + +x-common-auth-envs: &common-auth-envs + REMOTE_USER_HEADER_AUTHENTICATION_ENABLED: "true" + +x-db: &db + image: postgres:15 + read_only: true + networks: + - eurydice + environment: + POSTGRES_DB: eurydice + POSTGRES_USER: eurydice + POSTGRES_PASSWORD: eurydice + TZ: "Europe/Paris" + PGTZ: "Europe/Paris" + tmpfs: + - /tmp + - /run + command: + - "postgres" + - "-c" # turn on csv logging + - "logging_collector=on" + - "-c" + - "log_destination=csvlog" + - "-c" + - "log_directory=/var/log/postgresql" + - "-c" # filebeat format + - "log_line_prefix=%m [%p] %q%u@%d " + - "-c" # turn on hour-based log rotation + - "log_truncate_on_rotation=on" + - "-c" + - "log_filename=postgresql-%H.log" + - "-c" + - "log_rotation_age=60" + - "-c" + - "log_file_mode=0644" + - "-c" # print statements that took more than 200ms... + - "log_min_duration_statement=200" + - "-c" # ...do not print other statements... + - "log_statement=none" + - "-c" # ...do not even print the duration of these other statements + - "log_duration=off" + healthcheck: + # NOTE: DB will become healthy as soon as it has created the eurydice table + test: + - "CMD-SHELL" + - 'psql -U eurydice -lqtA | grep -q "^eurydice|"' + interval: 5s + timeout: 5s + retries: 5 + +services: + ################ Common Services ################ + + traefik: + image: traefik:2.10 + networks: + - eurydice + command: + - "--log.level=DEBUG" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:80" + - "--api.dashboard=true" + ports: + - "80:80" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro" + labels: + traefik.enable: "true" + traefik.http.routers.dashboard.rule: Host(`traefik.localhost`) + traefik.http.routers.dashboard.service: api@internal + + minio: + image: minio/minio:RELEASE.2023-08-23T10-07-06Z + networks: + - eurydice + environment: + MINIO_ROOT_USER: minio + MINIO_ROOT_PASSWORD: miniominio + volumes: + - minio-data:/data + - minio-config:/config + ports: + - 9000:9000 + command: + - server + - /data + - --config-dir + - /config/ + - --certs-dir + - /config/.certs/ + labels: + traefik.enable: "true" + traefik.http.routers.minio.rule: Host(`minio.localhost`) + healthcheck: + test: + - "CMD-SHELL" + - "curl http://127.0.0.1:9000/minio/health/live" + interval: 5s + timeout: 5s + retries: 10 + start_period: 2s + + frontend: + build: + context: "frontend" + dockerfile: docker/Dockerfile + target: dev + # note: we tag local dev images with our registry otherwise the image is tagged + # using the docker hub as a registry + # https://docs.docker.com/engine/reference/commandline/tag/#extended-description + image: eurydice/frontend:dev + networks: + - eurydice + volumes: + - ./frontend:/home/eurydice/frontend + command: + - make + - dev + environment: + NODE_ENV: development + # workaround for vue-cli container detection not working, see: + # https://github.com/vuejs/vue-cli/issues/6726 + CODESANDBOX_SSE: "1" + labels: + traefik.enable: "true" + traefik.http.routers.frontend.rule: Host(`origin.localhost`) || Host(`destination.localhost`) + traefik.http.routers.frontend.priority: "1" + + ################ Origin Services ################ + + backend-origin: + <<: *backend + depends_on: + db-origin: + condition: service_started + minio: + condition: service_healthy + volumes: + - ./backend:/home/eurydice/backend + - backend-origin-logs:/var/log/app + environment: + <<: [*backend-common-envs, *common-origin-envs, *common-auth-envs] + SECRET_KEY: GhSZiXkmYyty + EURYDICE_API: origin + ALLOWED_HOSTS: origin.localhost,backend-origin + CSRF_TRUSTED_ORIGINS: "" + DJANGO_MANAGEPY_MIGRATE: "on" + METRICS_SLIDING_WINDOW: ${METRICS_SLIDING_WINDOW:-60min} + healthcheck: + test: + - "CMD" + - /healthcheck.py + interval: 5s + timeout: 5s + retries: 5 + # NOTE: in the dev image the stat period is longer because we expect + # the container to apply migrations on first startup + # (in production this should be done by a different service) + start_period: 5s + labels: + traefik.enable: "true" + traefik.http.routers.backend-origin.rule: Host(`origin.localhost`) && PathPrefix(`/api`, `/admin`, `/static`) + command: + - make + - run-origin-api + + sender: + <<: *backend + depends_on: + # NOTE: we await for the backend container to be healthy + # because it is the one runnning the migrations in + # the developement environment + backend-origin: + condition: service_healthy + minio: + condition: service_healthy + lidis: + condition: service_started + volumes: + - ./backend:/home/eurydice/backend + - sender-logs:/var/log/app + extra_hosts: + - "lidis.docker.host:${LIDIS_DOCKER_HOST:-host-gateway}" + environment: + <<: [*backend-common-envs, *common-origin-envs] + DB_HOST: db-origin + MINIO_BUCKET_NAME: eurydice-origin + LIDIS_HOST: "lidis.docker.host" + LIDIS_PORT: "5000" + SENDER_RANGE_FILLER_CLASS: "UserRotatingTransferableRangeFiller" + command: + - make + - run-sender + + dbtrimmer-origin: + <<: *backend + depends_on: + # NOTE: we await for the backend container to be healthy + # because it is the one runnning the migrations in + # the developement environment + backend-origin: + condition: service_healthy + volumes: + - ./backend:/home/eurydice/backend + - dbtrimmer-origin-logs:/var/log/app + environment: + <<: [*backend-common-envs, *common-origin-envs] + DBTRIMMER_TRIM_TRANSFERABLES_AFTER: "2h" + DBTRIMMER_RUN_EVERY: "6h" + DBTRIMMER_POLL_EVERY: "200ms" + command: + - make + - run-origin-dbtrimmer + + db-origin: + <<: *db + ports: + - 5432:5432 + volumes: + - db-origin-data:/var/lib/postgresql/data + - db-origin-logs:/var/log/postgresql/:rw + + lidis: + image: eurydice/lidi-send:latest + network_mode: "host" + environment: + RUST_LOG: ${RUST_LOG:-INFO} + depends_on: + lidir: + condition: service_started + read_only: true + command: + - "--from_tcp" + - "0.0.0.0:5000" + - "--to_udp" + - "0.0.0.0:6000" + - "--nb_clients" + - "1" + + ################ Destination Services ################ + + lidir: + image: eurydice/lidi-receive:latest + network_mode: "host" + environment: + RUST_LOG: ${RUST_LOG:-INFO} + depends_on: + receiver: + condition: service_started + read_only: true + command: + - "--from_udp" + - "0.0.0.0:6000" + - "--to_tcp" + - "0.0.0.0:7000" + - "--nb_clients" + - "1" + + db-destination: + <<: *db + ports: + - 5433:5432 + volumes: + - db-destination-data:/var/lib/postgresql/data + - db-destination-logs:/var/log/postgresql/ + + receiver: + <<: *backend + depends_on: + # NOTE: we await for the backend container to be healthy + # because it is the one runnning the migrations in + # the developement environment + backend-destination: + condition: service_healthy + minio: + condition: service_healthy + volumes: + - ./backend:/home/eurydice/backend + - receiver-logs:/var/log/app + - destination-data-storage:/tmp/eurydice-data + environment: + <<: [*backend-common-envs, *common-destination-envs] + PACKET_RECEIVER_HOST: "0.0.0.0" + PACKET_RECEIVER_PORT: "7000" + RECEIVER_BUFFER_MAX_ITEMS: "4" + ports: + - "127.0.0.1:7000:7000" + command: + - make + - run-receiver + + dbtrimmer-destination: + <<: *backend + depends_on: + # NOTE: we await for the backend container to be healthy + # because it is the one runnning the migrations in + # the developement environment + backend-destination: + condition: service_healthy + volumes: + - ./backend:/home/eurydice/backend + - dbtrimmer-destination-logs:/var/log/app + - destination-data-storage:/tmp/eurydice-data + environment: + <<: [*backend-common-envs, *common-destination-envs] + DBTRIMMER_TRIM_TRANSFERABLES_AFTER: "2h" + DBTRIMMER_RUN_EVERY: "6h" + DBTRIMMER_POLL_EVERY: "200ms" + command: + - make + - run-destination-dbtrimmer + + s3remover: + <<: *backend + depends_on: + # NOTE: we await for the backend container to be healthy + # because it is the one runnning the migrations in + # the developement environment + backend-destination: + condition: service_healthy + minio: + condition: service_healthy + volumes: + - ./backend:/home/eurydice/backend + - s3remover-logs:/var/log/app + - destination-data-storage:/tmp/eurydice-data + environment: + <<: [*backend-common-envs, *common-destination-envs] + S3REMOVER_EXPIRE_TRANSFERABLES_AFTER: "1hour" + S3REMOVER_RUN_EVERY: "30s" + S3REMOVER_POLL_EVERY: "200ms" + command: + - make + - run-destination-s3remover + + backend-destination: + <<: *backend + depends_on: + db-destination: + condition: service_healthy + minio: + condition: service_healthy + volumes: + - ./backend:/home/eurydice/backend + - backend-destination-logs:/var/log/app + - destination-data-storage:/tmp/eurydice-data + environment: + <<: [*backend-common-envs, *common-destination-envs, *common-auth-envs] + SECRET_KEY: jBS0nmi4MOQIeXcN + EURYDICE_API: destination + ALLOWED_HOSTS: destination.localhost,backend-destination + CSRF_TRUSTED_ORIGINS: "" + DJANGO_MANAGEPY_MIGRATE: "on" + METRICS_SLIDING_WINDOW: ${METRICS_SLIDING_WINDOW:-60min} + healthcheck: + test: + - CMD + - /healthcheck.py + interval: 5s + timeout: 5s + retries: 5 + # NOTE: in the dev image the stat period is longer because we expect + # the container to apply migrations on first startup + # (in production this should be done by a different service) + start_period: 5s + labels: + traefik.enable: "true" + traefik.http.routers.backend-destination.rule: Host(`destination.localhost`) && PathPrefix(`/api`, `/admin`, `/static`) + command: + - make + - run-destination-api + + ################ Dev Tools ################ + + pgadmin: + build: + context: pgadmin + target: dev + # NOTE: we tag local dev images with our registry otherwise the image is tagged + # using the docker hub as a registry + # https://docs.docker.com/engine/reference/commandline/tag/#extended-description + image: eurydice/pgadmin4:dev + environment: + PGADMIN_DEFAULT_EMAIL: admin@example.com + PGADMIN_DEFAULT_PASSWORD: admin + PGADMIN_CONFIG_SERVER_MODE: "False" + PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False" + networks: + - eurydice + volumes: + - pgadmin:/var/lib/pgadmin + labels: + traefik.enable: "true" + traefik.http.routers.pgadmin.rule: Host(`pgadmin.localhost`) + +volumes: + db-origin-data: + db-origin-logs: + db-destination-data: + db-destination-logs: + minio-data: + minio-config: + pgadmin: + backend-origin-logs: + sender-logs: + dbtrimmer-origin-logs: + backend-destination-logs: + receiver-logs: + dbtrimmer-destination-logs: + s3remover-logs: + destination-data-storage: + +networks: + eurydice: + driver: bridge + name: eurydice + ipam: + config: + - subnet: ${EURYDICE_IPV4_SUBNET:-172.18.0.0/24} diff --git a/docs/administrators.md b/docs/administrators.md new file mode 100644 index 0000000..2692f82 --- /dev/null +++ b/docs/administrators.md @@ -0,0 +1,233 @@ +# 🚀 Deployment in production and administration + +Instructions to deploy Eurydice manually are available below. + +## 👷‍♀️ Manual deployment with docker compose + +- `compose.yml` allows for launching a local development environment + - ```bash + docker compose up -d + ``` +- `compose.prod.yml` allows for launching the origin or destination production stack + + 1. Configure the environment variables ([more details below](#️-environment-variables)) + 2. Create the directories configured in the environment variables + 3. Set permissions to allow read/write access to these directories for the UID configured in the environment variables + + **If you don't need elk logging:** + + 4. Configure the database by applying the migrations: + - Origin: + ```bash + docker compose -f compose.prod.yml --profile origin run --rm db-migrations-origin + ``` + - Destination (on another machine): + ```bash + docker compose -f compose.prod.yml --profile destination run --rm db-migrations-destination + ``` + 5. Launch the stack: + + - Origin: + ```bash + docker compose -f compose.prod.yml --profile origin up -d + ``` + - Destination (on another machine): + ```bash + docker compose -f compose.prod.yml --profile destination up -d + ``` + + **If you want elk logging:** + + 4. Configure the database by applying the migrations: + - Origin: + ```bash + docker compose -f compose.prod.yml --profile origin-with-elk-logging run --rm db-migrations-origin-with-elk + ``` + - Destination (on another machine): + ```bash + docker compose -f compose.prod.yml --profile destination-with-elk-logging run --rm db-migrations-destination-with-elk + ``` + 5. Launch the stack: + + - Origin: + ```bash + docker compose -f compose.prod.yml --profile origin-with-elk-logging up -d + ``` + - Destination (on another machine): + ```bash + docker compose -f compose.prod.yml --profile destination-with-elk-logging up -d + ``` + - Note: Additional environment variables are needed: see the list below. You will need an API key: + [more details below](#elasticsearch-api-key). + + **Optional admin user** + + 6. (optional) Create an administrator user for accessing the admin interface at `/admin` (default credentials are admin/admin) + - Origin: + ```bash + docker compose -f compose.prod.yml exec backend-origin make superuser + ``` + - Destination (on another machine): + ```bash + docker compose -f compose.prod.yml exec backend-destination make superuser + ``` + +## ⚙️ Environment Variables + +The `.env` file should be placed in the directory from which you are running `docker compose` (usually next to the `compose.yml` file). +You can also name the file as you wish [and point compose to it using the `--env-file` flag](https://docs.docker.com/compose/environment-variables/#using-the---env-file--option). + +The following environment variables should be configured for the deployment (variables without a default value are mandatory): + +| Variable | Default value | Description | +| ------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `EURYDICE_VERSION` | `latest` | Tag for the Eurydice docker image ([list of tags](https://github.com/ANSSI-FR/eurydice/tags)) | +| `LIDI_VERSION` | `latest` | Tag for the lidis/r docker images ([list of tags](https://github.com/ANSSI-FR/lidi/tags)) | +| `LOCAL_DOCKER_REGISTRY` | | Docker image registry: hostname of the registry that hosts Docker images for eurydice and lidi | +| `REMOTE_DOCKER_REGISTRY` | `docker.io` | Docker image registry: hostname for other docker images (e.g. https://hub.docker.com/) | +| `UID` | `1000` | Numeric identifier to the user under which the services of will run | +| `GID` | `1000` | Numeric identifier for the group under which the services will run | +| `GUNICORN_CONFIGURATION` | `""` | Gunicorn commandline arguments | +| `EURYDICE_IPV4_SUBNET` | `172.18.0.0/24` | Range of IP addresses that will be assigned to containers within the eurydice network | +| `BACKEND_HOSTNAMES` | `localhost` | Hostname(s) for the backend API separated by a comma: `,` | +| `CSRF_TRUSTED_ORIGINS` | `""` | Additionnal origin(s) for CSRF validation, separated by a comma: `,`. SEE [Django documentation](https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins) Must include scheme ("http://", "https://") . May be left empty. | +| `FRONTEND_HOSTNAMES` | `localhost` | Hostname(s) for the frontend UI separated by a space: ` ` | +| `KIBANA_HOSTNAME` | `localhost` | Hostname for the Kibana UI. | +| `LIDIR_HOST` | `0.0.0.0`(lidir) | Lidir hostname | +| `LIDIR_PORT` | `11011` | Lidir port number | +| `LIDI_UDP_MTU` | `1500` | Value for lidi option `--from_udp_mtu` (see https://github.com/ANSSI-FR/lidi/blob/master/doc/parameters.rst#block-and-packet-sizes) | +| `LIDI_ENCODING_BLOCK_SIZE` | `60000` | Value for lidi option `--encoding_block_size` (see https://github.com/ANSSI-FR/lidi/blob/master/doc/parameters.rst#block-and-packet-sizes) | +| `LIDI_REPAIR_BLOCK_SIZE` | `6000` | Value for lidi option `--repair_block_size` (see https://github.com/ANSSI-FR/lidi/blob/master/doc/parameters.rst#block-and-packet-sizes) | +| `LIDIS_DOCKER_HOST` | `0.0.0.0`(lidis), `host-gateway`(sender) | Host interface on which lidis will listen and to which the sender will connect | +| `MINIO_ENABLED` | `true` | Enables usage of minio. If minio is disabled, eurydice will store transferable data on the filesystem. See `TRANSFERABLE_STORAGE_DIR` and `MINIO_ENDPOINT`. **Disabling minio and using the filesystem is only supported on the destination side.** | +| `TRANSFERABLE_STORAGE_DIR` | | Host path to the directory containing transferable data, if minio is disabled data | +| `MINIO_ENDPOINT` | `minio:9000` | Hostname and port of the s3 storage server. By default, uses the minio container embedded in the compose stack | +| `MINIO_DATA_DIR` | | Host path to the directory containing minio data | +| `MINIO_CONF_DIR` | | Host path to the directory containing the minio configuration | +| `DB_DATA_DIR` | | Host path to the directory containing the database's data | +| `LOG_LEVEL` | `INFO` | Django and Eurydice Log Level | +| `LOG_TO_FILE` | `true` | If true, python processes will configure their logger to log into json files. See `PYTHON_LOGS_DIR` . | +| `PYTHON_LOGS_DIR` | | Host path to the directory containing python logs. Each service will write into their own sub-directory, as such: `${PYTHON_LOGS_DIR}/sender-logs/log.json` | +| `RUST_LOG` | See compose.prod.yml | Rust log level (i.e. Lidi log level) | +| `DB_LOGS_DIR` | | Host path to the directory containing the database's logs | +| `ELK_VERSION` | `7.17.11` | Version of Filebeat service. Should match the version of the targeted elastic service. | +| `FILEBEAT_CONFIG_PATH` | | Host path to the filebeat config. Set this variable if you use the filebeat service (`--profile *-with-elk-logging`). | +| `FILEBEAT_LOGS_DIR` | | Host path to the directory [containing logs created by filebeat](https://www.elastic.co/guide/en/beats/filebeat/8.0/directory-layout.html) . Set this variable if you use the filebeat service (`--profile *-with-elk-logging`). | +| `FILEBEAT_DATA_DIR` | | Host path to the directory [containing filebeat's persistent data files](https://www.elastic.co/guide/en/beats/filebeat/8.0/directory-layout.html) . Set this variable if you use the filebeat service (`--profile *-with-elk-logging`). | +| `ELASTICSEARCH_API_KEY` | `""` | API key used by filebeat to publish logs to elasticsearch. Set this variable if you use the filebeat service (`--profile *-with-elk-logging`). | +| `ELASTICSEARCH_HOSTS` | | Elasticsearch URL. Set this variable if you use the filebeat service (`--profile *-with-elk-logging`). Should start with https. | +| `ELASTICSEARCH_CERT_PATH` | `/etc/ssl/certs/ca-certificates.crt` | Absolute path to certificate that should be used to connect to elastic search. Set this variable if you use the filebeat service (`--profile *-with-elk-logging`). | +| `MINIO_SECRET_KEY` | | Secret key for Minio's S3 API | +| `MINIO_EXPIRATION_DAYS` | `8` | Expiration delay for Minio objects, in days, after which they will automatically be deleted on the origin side. Default is 8. (On the destination side, this expiration delay is automatically computed to be `S3REMOVER_EXPIRE_TRANSFERABLES_AFTER` + 1 day, rounded down a day.) | +| `EURYDICE_TRANSFERABLE_MAX_SIZE` | `54975581388800` | File upload size limit on the origin side. Should be less than your storage capacity. Default is 50TiB, the [maximum size that Minio can handle](https://min.io/docs/minio/linux/operations/checklists/thresholds.html#minio-server-limits). | +| `DJANGO_SECRET_KEY` | | [Django secret key](https://docs.djangoproject.com/en/3.2/ref/settings/#secret-key) django | +| `DB_PASSWORD` | | PostgreSQL database password d'Eurydice | +| `USER_ASSOCIATION_TOKEN_SECRET_KEY` | | Secret key for generating association tokens (must be the same for the origin and destination APIs) | +| `S3REMOVER_RUN_EVERY` | `1min` | Run frequency for the S3Remover (service which removes minio objects and sets Transferable to `EXPIRED` on the destination side) | +| `S3REMOVER_EXPIRE_TRANSFERABLES_AFTER` | `7days` | Duration before which files received on the destination side are marked as `EXPIRED`. | +| `S3REMOVER_POLL_EVERY` | `200ms` | Maximum acceptable duration between the S3Remover receiving a `SIGINT` signal and the process' termination | +| `DBTRIMMER_RUN_EVERY` | `6h` | Launch frequency for the DBTrimmer (service which removes old transferables from the database) | +| `DBTRIMMER_TRIM_TRANSFERABLES_AFTER` | `7days` | Availability duration for a transferable's metadata once it has been sent, received, or if either have failed (after this duration, a transferable will 404 if request) | +| `DBTRIMMER_POLL_EVERY` | `200ms` | Maximum acceptable duration between the DBTrimmer receiving a `SIGINT` signal and the process' termination | +| `SENDER_RANGE_FILLER_CLASS` | `UserRotatingTransferableRangeFiller` | Changes the Sender's Transferable fetch strategy. Available choices are `UserRotatingTransferableRangeFiller` (default, attempts to fairly distribute bandwidth for Transferables among Users) or `FIFOTransferableRangeFiller` (faster implementation that ignores User priority; good for single-user usages). | +| `REMOTE_USER_HEADER_AUTHENTICATION_ENABLED` | `False` | Enable authentication through the HTTP header set by `HTTP_X_REMOTE_USER` [**⚠️ beware of associated security implications**](#️-security-risks-associated-with-http-header-authentication) | +| `REMOTE_USER_HEADER` | `HTTP_X_REMOTE_USER` | Select the remote user authentication method, note the `HTTP_` prefix for HTTP header based authentication | +| `THROTTLE_RATE` | `30/second` | File upload rate limiting | +| `RECEIVER_BUFFER_MAX_ITEMS` | `4` | Maximum amount of incoming transferables awaiting processing that the receiver can hold before dropping incoming data. Should roughly match (`MEM_LIMIT_RECEIVER` / 2 \* `TRANSFERABLE_RANGE_SIZE`) | +| `CPUS_BACKEND` | See compose.prod.yml | CPU docker limit for each backend service | +| `CPUS_DATABASE` | See compose.prod.yml | CPU docker limit for database service | +| `CPUS_` | See compose.prod.yml | CPU docker limit | +| `MEM_LIMIT_BACKEND` | See compose.prod.yml | Memory docker limit for each backend service | +| `MEM_LIMIT_DATABASE` | See compose.prod.yml | Memory docker limit for database service | +| `MEM_LIMIT_` | See compose.prod.yml | Memory docker limit | +| `SHM_SIZE_DATABASE` | See compose.prod.yml | Shared memory docker limit for the database container | +| `EURYDICE_CONTACT` | See compose.prod.yml | Contact information, useful to report bugs for example. It is displayed in the API documentation. | +| `EURYDICE_CONTACT_FR` | See compose.prod.yml | Contact information in French. It is displayed in the frontend. | +| `UI_BADGE_CONTENT` | See compose.prod.yml | Configurable banner to be displayed on the front page in the frontend. | +| `UI_BADGE_COLOR` | See compose.prod.yml | Color for the configurable frontpage banner. List of available colors: https://vuetifyjs.com/en/styles/colors/#material-colors | +| `METRICS_SLIDING_WINDOW` | See compose.prod.yml | Duration used to compute metrics for /metrics endpoints. | +| `USER_ASSOCIATION_TOKEN_EXPIRES_AFTER` | See compose.prod.yml | Expiration delay of user association token. | +| `TRANSFERABLE_HISTORY_DURATION` | | Duration of the history. | +| `TRANSFERABLE_HISTORY_SEND_EVERY` | | Frequency of the history. | +| `HTTP_SERVICES_BIND_HOST` | 127.0.0.1 | TCP bind address for docker services. | + +**Warning: make sure that `DBTRIMMER_TRIM_TRANSFERABLES_AFTER` is greater than `TRANSFERABLE_HISTORY_DURATION`.** +If `DBTRIMMER_TRIM_TRANSFERABLES_AFTER` is less than `TRANSFERABLE_HISTORY_DURATION`, the DBTrimmer on the destination side will remove transferables that the history from the origin will recreate. +This will lead to previously deleted transferables being marked as `ERROR` on the destination side. + +### History management + +In some cases, the origin-side database may end up holding millions of Transferable entries. Combined with a long history duration, this may lead to the sender generating enormous quantities of data, just to send the history. + +If that happens, you may want to consider using a much smaller history duration, while keeping the same `DBTRIMMER_TRIM_TRANSFERABLES_AFTER`. This will allow you to manually ask the sender to send a complete history only when needed, with the following command: + +```bash +docker compose -f compose.prod.yml exec sender python manage.py send_history --duration 7days +``` + +## 👤 UID, GID and bind mounts + +The `UID` and `GID` variables need not be any particular ID, the only requirement is that these UID/GID have read/write permissions on the directories defined in the other variables. + +### Storing data + +If `MINIO_ENABLED` is false, Eurydice will attempt to use the filesystem as storage, instead of a remote s3 storage server. `TRANSFERABLE_STORAGE_DIR` should be set to the path to a directory. + +### Logging + +If `LOG_TO_FILE` is true, Eurydice will write additional logs at `${PYTHON_LOGS_DIR}/-logs/log.json`. This is useful when using a `-with-elk-logging` profile, so that filebeat may read, process and share applicative logs to a remote elk server. + +## 🖥️ Reverse Proxy + +Eurydice needs to be setup behind a reverse proxy to work securely and optimally. +Setting up a reverse proxy will also enable authentication at the reverse proxy level rather than relying on the application's authentication mechanism. + +The application's services are exposed on `localhost` by the `compose.prod.yml`. +It is advised to route them like so: + +- Web UI at `http://localhost:8888` + - Should handle requests who don't match the rules for the services below +- API at `http://localhost:8080` + - Should handle requests whose path is prefixed by `/api` `/admin` `/static` +- Minio at `http://localhost:9000` + - Should handle requests whose path is prefixed by `/minio` (the `/minio` prefix should be removed by the reverse proxy) +- pgadmin at `http://localhost:5050` + - Should handle requests whose path is prefixed by `/pgadmin` (the `/pgadmin` prefix should be removed by the reverse proxy) + - The reverse proxy should only allow authenticated users on this endpoint as all pgadmin authentication is disabled + +The reverse proxy should also serve all endpoints over TLS. +It should also set the `X-Forwarded-Proto`, `X-Forwarded-For` and `X-Forwarded-Host` headers to forward information from the original request to the services. + +### ⚠️ Security risks associated with HTTP header authentication + +**Warning: [django normalizes HTTP headers](https://django.readthedocs.io/en/stable/releases/1.6.10.html#wsgi-header-spoofing-via-underscore-dash-conflation). So it is important to make sure that a user cannot forge an authentication HTTP header that would be considered safe by the reverse proxy, but would in fact be normalized by Django into a valid authentication header** + +This risk can be mitigated by configuring which header is used for authenticating the header with the `REMOTE_USER_HEADER` variable. +This environment variable could for example be set to a purely alphanumeric value (not affected by normalization) which could not easily be guessed by the user. +In such a scenario, one would still need to make sure that the reverse proxy correctly prevents users from submitting their own authentication header. + +### 🛣️ Basic authentication + +**Warning: Basic HTTP authentication passes your credentials over the network as clear text. As such, it is NOT the recommended method of authentication. If you need to run Eurydice using basic auth, at least use HTTPS.** + +In case you are unable to setup a reverse proxy and remote user authentication, Eurydice may support [Basic HTTP Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme). + +Basic HTTP Authentication is enabled when the `REMOTE_USER_HEADER_AUTHENTICATION_ENABLED` variable is set to `false`. + +### Elasticsearch API key + +To send applicative logs into an ELK you need an API key for a user with following role restrictions: + +``` +{ + "filebeat_writer": { + "cluster": ["monitor", "read_ilm", "read_pipeline"], + "index": [ + { + "names": ["eurydice-*"], + "privileges": ["view_index_metadata", "create_doc", "auto_configure"] + } + ] + } +} +``` diff --git a/docs/developers.md b/docs/developers.md new file mode 100644 index 0000000..3fe5b22 --- /dev/null +++ b/docs/developers.md @@ -0,0 +1,79 @@ +# 👩‍💻 Development + +## 🏁 Usage + +### 🚀 Run the development environment + +Start the container used for development in detached mode. + +```bash +# Development stack with basic console logging +docker compose up -d + +# Development stack with advanced ElasticSearch logging using filebeat (accessible at `kibana.localhost`) +docker compose -f compose.yml -f compose.kibana.yml up -d +``` + +_Note: it is [recommended](https://github.com/moby/moby/issues/40379) to use the environment variable `DOCKER_BUILDKIT=1`_ + +The following URLs are available in the development environment: + +- http://origin.localhost: the origin user interface. +- http://origin.localhost/api/docs/: the origin API documentation. +- http://origin.localhost/api/v1/: the origin API. +- http://origin.localhost/admin/: the origin administration interface. +- http://destination.localhost: the destination user interface. +- http://destination.localhost/api/docs/: the destination API documentation. +- http://destination.localhost/api/v1/: the destination API. +- http://destination.localhost/admin/: the destination administration interface. +- http://minio.localhost/: the Minio to store the origin and destination files. +- http://pgadmin.localhost/: database management tool. +- http://traefik.localhost/: the traefik dashboard + +### Start the production stack locally + +You may use the example environment variables defined in `.env.example`. +Create needed directories on you host: + +```bash +mkdir /tmp/eurydice +mkdir /tmp/eurydice/minio-data +mkdir /tmp/eurydice/minio-conf +mkdir /tmp/eurydice/db-data +mkdir /tmp/eurydice/db-logs +mkdir /tmp/eurydice/filebeat-logs +mkdir /tmp/eurydice/filebeat-data +sudo chown -R 1000:1000 /tmp/eurydice/ +``` + +Then, run database migrations: + +```bash +docker compose -f compose.prod.yml --profile origin --env-file .env.example run --rm db-migrations-origin +``` + +or + +```bash +docker compose -f compose.prod.yml --profile destination --env-file .env.example run --rm db-migrations-destination +``` + +Finally, you can start the stack: + +```bash +docker compose -f compose.prod.yml --env-file .env.example --profile origin up -d +``` + +or + +```bash +docker compose -f compose.prod.yml --env-file .env.example --profile destination up -d +``` + +## ✨ Frontend + +The developer documentation for the frontend of eurydice is available here: [`frontend/README.md`](../frontend/README.md) + +## 🖥️ Backend + +The developer documentation for the backend of eurydice is available here: [`backend/README.md`](../backend/README.md) diff --git a/filebeat/filebeat.destination.yml b/filebeat/filebeat.destination.yml new file mode 100644 index 0000000..ec0e01e --- /dev/null +++ b/filebeat/filebeat.destination.yml @@ -0,0 +1,80 @@ +filebeat.modules: + - module: postgresql + log: + enabled: true + var.paths: + - /logs/destination/postgresql/*.csv + input: + processors: + - add_fields: + target: 'service' + fields: + side: destination + +x-common-input-conf: &common-input-conf + type: filestream + enabled: true + fields_under_root: true + parsers: + - ndjson: + target: "data" + add_error_key: true + message_key: "message" + +filebeat.inputs: +- <<: *common-input-conf + id: backend-destination-logs + paths: + - /logs/destination/backend/log.json + fields: + service: + side: destination + type: backend +- <<: *common-input-conf + id: receiver-logs + paths: + - /logs/destination/receiver/log.json + fields: + service: + side: destination + type: receiver +- <<: *common-input-conf + id: dbtrimmer-destination-logs + paths: + - /logs/destination/dbtrimmer/log.json + fields: + service: + side: destination + type: dbtrimmer +- <<: *common-input-conf + id: s3remover-destination-logs + paths: + - /logs/destination/s3remover/log.json + fields: + service: + side: destination + type: s3remover +- <<: *common-input-conf + id: db-migrations-destination-logs + paths: + - /logs/destination/db-migrations/log.json + fields: + service: + side: destination + type: db-migrations + +setup: + template: + enabled: true + name: filebeat-%{[agent.version]} + pattern: "eurydice-*" + ilm: + enabled: false + +output.elasticsearch: + hosts: "${ELASTICSEARCH_HOSTS}" + api_key: "${ELASTICSEARCH_API_KEY:}" + index: "eurydice-%{[service.side]}-%{[service.type]}-1.0" + ssl: + certificate_authorities: + - /usr/share/elasticsearch/config/certs/cert.crt diff --git a/filebeat/filebeat.null.yml b/filebeat/filebeat.null.yml new file mode 100644 index 0000000..32af3c6 --- /dev/null +++ b/filebeat/filebeat.null.yml @@ -0,0 +1,7 @@ +filebeat.inputs: + - type: filestream + paths: + - /dev/null + +output.console: + pretty: true diff --git a/filebeat/filebeat.origin.yml b/filebeat/filebeat.origin.yml new file mode 100644 index 0000000..2f71e39 --- /dev/null +++ b/filebeat/filebeat.origin.yml @@ -0,0 +1,72 @@ +filebeat.modules: + - module: postgresql + log: + enabled: true + var.paths: + - /logs/origin/postgresql/*.csv + input: + processors: + - add_fields: + target: 'service' + fields: + side: origin + +x-common-input-conf: &common-input-conf + type: filestream + enabled: true + fields_under_root: true + parsers: + - ndjson: + target: "data" + add_error_key: true + message_key: "message" + +filebeat.inputs: +- <<: *common-input-conf + id: backend-origin-logs + paths: + - /logs/origin/backend/log.json + fields: + service: + side: origin + type: backend +- <<: *common-input-conf + id: sender-logs + paths: + - /logs/origin/sender/log.json + fields: + service: + side: origin + type: sender +- <<: *common-input-conf + id: dbtrimmer-origin-logs + paths: + - /logs/origin/dbtrimmer/log.json + fields: + service: + side: origin + type: dbtrimmer +- <<: *common-input-conf + id: db-migrations-origin-logs + paths: + - /logs/origin/db-migrations/log.json + fields: + service: + side: origin + type: db-migrations + +setup: + template: + enabled: true + name: filebeat-%{[agent.version]} + pattern: "eurydice-*" + ilm: + enabled: false + +output.elasticsearch: + hosts: "${ELASTICSEARCH_HOSTS}" + api_key: "${ELASTICSEARCH_API_KEY:}" + index: "eurydice-%{[service.side]}-%{[service.type]}-1.0" + ssl: + certificate_authorities: + - /usr/share/elasticsearch/config/certs/cert.crt diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..b6f7adf --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,11 @@ +* +!docker/ +!public/ +!src/ +!tests/ +!Makefile +!package.json +!package-lock.json +!.prettierignore +!vue.config.js +!.npmrc diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..2b008da --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1 @@ +node-options="--openssl-legacy-provider" diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 0000000..1eae0cf --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/frontend/Makefile b/frontend/Makefile new file mode 100644 index 0000000..427b9a4 --- /dev/null +++ b/frontend/Makefile @@ -0,0 +1,55 @@ +.PHONY: install +install: ## Install dependencies. + npm ci + +.PHONY: build-origin +build-origin: ## Build origin for production environment + npm run build:origin + +.PHONY: build-destination +build-destination: ## Build destination for production environment + npm run build:destination + +.PHONY: build +build: ## Build for production environment + $(MAKE) build-origin build-destination + +.PHONY: dev +dev: ## Run development server. + npm run dev + +.PHONY: prod-origin +prod-origin: ## Serve origin production bundle. WARNING: do not use this in production, this is only intended for testing the production bundle + npm run prod:origin + +.PHONY: prod-destination +prod-destination: ## Serve destination production bundle. WARNING: do not use this in production, this is only intended for testing the production bundle + npm run prod:destination + +.PHONY: lint +lint: ## Runs a static analysis of the code. + npm run lint + npm run lint:css + +.PHONY: lint-fix +lint-fix: ## Runs a static analysis of the code and fix linting errors. + npm run lint:fix + npm run lint:css:fix + +.PHONY: audit +audit: ## Checks production dependencies for vulnerabilities. + npm audit --production + +.PHONY: format +format: ## Format CSS, SCSS, YAML, JSON and Markdown files. + npm run format + +.PHONY: tests +tests: ## Runs unit tests. + npm run tests + +.PHONY: help +help: ## Show this help. + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.DEFAULT_GOAL := help diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..03e2c9f --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,33 @@ +# ✨ Eurydice frontend + +## 🧪 Testing + +Unit tests are performed with [Jest](https://jestjs.io/). + +Run the tests with the following command: + +```bash +make tests +``` + +## 👮‍♀️ Code formatting and 🔎 Static analysis + +[ESLint](https://eslint.org/) is the project's linter. +The [Prettier](https://prettier.io/) formatter is integrated in the linter. +You can Launch them with the following command: + +```bash +make lint +``` + +To automatically fix errors detected by the linter, use the following command: + +```bash +make lint-fix +``` + +CSS, SCSS, YAML, JSON and Markdown files can be formatted with the following command: + +```bash +make format +``` diff --git a/frontend/docker/Dockerfile b/frontend/docker/Dockerfile new file mode 100644 index 0000000..9c23533 --- /dev/null +++ b/frontend/docker/Dockerfile @@ -0,0 +1,84 @@ +ARG EURYDICE_VERSION development + +# ------------------------------------------------------------------------------ +# Base +# ------------------------------------------------------------------------------ + +FROM node:18-bullseye AS base + +ENV PATH /home/eurydice/frontend/node_modules/.bin:$PATH + +# vue-cli uses the VUE_CLI_BABEL_TRANSPILE_MODULES environment variable, +# to control whether the babel-plugin-dynamic-import-node plugin is enabled. +# It only does one thing by converting all import() to require(). +# This configuration can significantly increase the speed of hot updates, +# when you have a large number of pages. +# Detail: https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/babel-preset-app/index.js + +ENV VUE_CLI_BABEL_TRANSPILE_MODULES true + +WORKDIR /home/eurydice/frontend + +# ------------------------------------------------------------------------------ +# Development +# ------------------------------------------------------------------------------ + +FROM base AS dev + +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +EXPOSE 8080 + +ENTRYPOINT ["/entrypoint.sh"] + +CMD ["make", "dev"] + +# ------------------------------------------------------------------------------ +# CI +# ------------------------------------------------------------------------------ + +FROM base AS ci + +RUN chown node:node "$(pwd)" + +USER node + +COPY --chown=node:node . . + +RUN make install + +# ------------------------------------------------------------------------------ +# Build production dependencies +# ------------------------------------------------------------------------------ + +FROM ci AS build-prod + +ARG EURYDICE_VERSION + +ENV NODE_ENV production +ENV VUE_APP_VERSION ${EURYDICE_VERSION} + +RUN make build + +# ------------------------------------------------------------------------------ +# Production image +# ------------------------------------------------------------------------------ + +FROM nginx:1-alpine as prod + +## rewrite nginx user UID and GID to 1000:1000 +RUN sed -i 's/nginx:x:101:101:nginx/nginx:x:1000:1000:nginx/g' /etc/passwd \ + && chown -R nginx:nginx /etc/nginx + +COPY --chown=nginx:nginx docker/nginx.conf /etc/nginx/nginx.conf +COPY --chown=nginx:nginx docker/default.conf.template /etc/nginx/templates/default.conf.template + +# switch to NGINX user +USER nginx + +EXPOSE 8080 + +# copy the dist folder built during the build stage +COPY --from=build-prod --chown=nginx:nginx \ + /home/eurydice/frontend/dist /var/www/html/eurydice diff --git a/frontend/docker/default.conf.template b/frontend/docker/default.conf.template new file mode 100644 index 0000000..c6f192a --- /dev/null +++ b/frontend/docker/default.conf.template @@ -0,0 +1,36 @@ +server { + # if no host matched, close the connection to prevent host spoofing + listen 8080 default_server; + return 444; +} + +server { + listen 8080; + server_name ${NGINX_SERVER_NAME}; + + root /var/www/html/eurydice/${NGINX_ROOT_DIR}; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + keepalive_timeout 5; + + gzip on; + gzip_types + text/plain + text/css + text/js + text/javascript + application/javascript + image/svg+xml; + + add_header Strict-Transport-Security "max-age=315360000"; + + add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), interest-cohort=()"; + add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self'; connect-src 'self'; frame-ancestors 'none'"; + add_header Referrer-Policy "same-origin"; + add_header X-Content-Type-Options "nosniff"; + +} diff --git a/frontend/docker/entrypoint.sh b/frontend/docker/entrypoint.sh new file mode 100644 index 0000000..199d2a5 --- /dev/null +++ b/frontend/docker/entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +# install dependencies on first start +if [ ! -d "$(pwd)/node_modules" ]; then + make install +fi + +exec "$@" diff --git a/frontend/docker/nginx.conf b/frontend/docker/nginx.conf new file mode 100644 index 0000000..5cb56a8 --- /dev/null +++ b/frontend/docker/nginx.conf @@ -0,0 +1,41 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; + +# Move NGINX pid to allow running NGINX as unprivileged user +pid /tmp/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + # Move temporary directories to allow running NGINX as unprivileged user + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp_path; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + server_tokens off; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + + keepalive_timeout 65; + + include /etc/nginx/conf.d/*.conf; + + server_names_hash_bucket_size 128; +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..afba02a --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,22677 @@ +{ + "name": "eurydice", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "eurydice", + "version": "0.1.0", + "dependencies": { + "@mdi/js": "~7.3.67", + "axios": "~1.7.7", + "bytes": "~3.1.2", + "core-js": "~3.23.3", + "dayjs": "~1.11.10", + "lodash": "~4.17.21", + "typeface-roboto": "~1.1.13", + "vue": "^2.7.0", + "vue-router": "~3.5.3", + "vuetify": "^2.6.15", + "vuex": "~3.6.2" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "~4.5.18", + "@vue/cli-plugin-eslint": "~4.5.18", + "@vue/cli-plugin-router": "~4.5.18", + "@vue/cli-plugin-unit-jest": "~4.5.18", + "@vue/cli-plugin-vuex": "~4.5.18", + "@vue/cli-service": "~4.5.18", + "@vue/eslint-config-airbnb": "~5.3.0", + "@vue/test-utils": "~1.3.0", + "babel-eslint": "~10.1.0", + "babel-plugin-lodash": "~3.3.4", + "eslint": "~6.8.0", + "eslint-config-prettier": "~6.15.0", + "eslint-plugin-import": "~2.25.4", + "eslint-plugin-prettier": "~3.4.1", + "eslint-plugin-vue": "~8.5.0", + "jest-junit": "~13.0.0", + "prettier": "~2.5.1", + "sass": "~1.32.13", + "sass-loader": "~10.2.1", + "stylelint": "~13.13.1", + "stylelint-config-prettier": "~8.0.2", + "stylelint-config-recommended": "~5.0.0", + "stylelint-config-sass-guidelines": "~8.0.0", + "stylelint-config-suitcss": "~17.0.0", + "stylelint-order": "~4.1.0", + "stylelint-prettier": "~1.2.0", + "stylelint-scss": "~3.20.1", + "stylelint-webpack-plugin": "~2.2.2", + "vue-cli-plugin-vuetify": "~2.4.8", + "vue-template-compiler": "^2.7.0", + "vuetify-loader": "~1.7.3" + } + }, + "node_modules/@achrinza/node-ipc": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@achrinza/node-ipc/-/node-ipc-9.2.2.tgz", + "integrity": "sha512-b90U39dx0cU6emsOvy5hxU4ApNXnE3+Tuo8XQZfiKTGelDwpMwBVgBP7QX6dGTcJgu/miyJuNJ/2naFBliNWEw==", + "dev": true, + "dependencies": { + "@node-ipc/js-queue": "2.0.3", + "event-pubsub": "4.3.0", + "js-message": "1.0.7" + }, + "engines": { + "node": "8 || 10 || 12 || 14 || 16 || 17" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.25.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.8.tgz", + "integrity": "sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.8.tgz", + "integrity": "sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helpers": "^7.25.7", + "@babel/parser": "^7.25.8", + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.8", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.7.tgz", + "integrity": "sha512-12xfNeKNH7jubQNm7PAkzlLwEmCs1tfuX3UjIw6vP6QXi+leKh6+LyC/+Ed4EIQermwd58wsyh070yjDHFlNGg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.7.tgz", + "integrity": "sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/traverse": "^7.25.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.7.tgz", + "integrity": "sha512-byHhumTj/X47wJ6C6eLpK7wW/WBEcnUeb7D0FNc/jFQnQVw7DOso3Zz5u9x/zLrFVkHa89ZGDbkAa1D54NdrCQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "regexpu-core": "^6.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.7.tgz", + "integrity": "sha512-O31Ssjd5K6lPbTX9AAYpSKrZmLeagt9uwschJd+Ixo6QiRyfpvgtVQp8qrDR9UNFjZ8+DO34ZkdrN+BnPXemeA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.7.tgz", + "integrity": "sha512-VAwcwuYhv/AT+Vfr28c9y6SHzTan1ryqrydSTFGjU0uDJHw3uZ+PduI8plCLkRsDnqK2DMEDmwrOQRsK/Ykjng==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", + "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.7.tgz", + "integrity": "sha512-kRGE89hLnPfcz6fTrlNU+uhgcwv0mBE4Gv3P9Ke9kLVJYpi4AMVVEElXvB5CabrPZW4nCM8P8UyyjrzCM0O2sw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-wrap-function": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.7.tgz", + "integrity": "sha512-iy8JhqlUW9PtZkd4pHM96v6BdJ66Ba9yWSE4z0W4TvSZwLBPkyDsiIU3ENe4SmrzRBs76F7rQXTy1lYC49n6Lw==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.7.tgz", + "integrity": "sha512-pPbNbchZBkPMD50K0p3JGcFMNLVUCuU/ABybm/PGNj4JiHrpmNyqqCphBk4i19xXtNV0JhldQJJtbSW5aUvbyA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.7.tgz", + "integrity": "sha512-MA0roW3JF2bD1ptAaJnvcabsVlNQShUaThyJbCDD4bCp8NEgiFvpoqRI2YS22hHlc2thjO/fTg2ShLMC3jygAg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.7.tgz", + "integrity": "sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", + "dependencies": { + "@babel/types": "^7.25.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.7.tgz", + "integrity": "sha512-UV9Lg53zyebzD1DwQoT9mzkEKa922LNUp5YkTJ6Uta0RbyXaQNUgcvSt7qIu1PpPzVb6rd10OVNTzkyBGeVmxQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.7.tgz", + "integrity": "sha512-GDDWeVLNxRIkQTnJn2pDOM1pkCgYdSqPeT1a9vh9yIqu2uzzgw1zcqEb+IJOhy+dTBMlNdThrDIksr2o09qrrQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.7.tgz", + "integrity": "sha512-wxyWg2RYaSUYgmd9MR0FyRGyeOMQE/Uzr1wzd/g5cf5bwi9A4v6HFdDm7y1MgDtod/fLOSTZY6jDgV0xU9d5bA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.7.tgz", + "integrity": "sha512-Xwg6tZpLxc4iQjorYsyGMyfJE7nP5MV8t/Ka58BgiA7Jw0fRqQNcANlLfdJ/yvBt9z9LD2We+BEkT7vLqZRWng==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/plugin-transform-optional-chaining": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.7.tgz", + "integrity": "sha512-UVATLMidXrnH+GMUIuxq55nejlj02HP7F5ETyBONzP6G87fPBogG4CH6kxrSrdIuAjdwNO9VzyaYsrZPscWUrw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.25.7.tgz", + "integrity": "sha512-q1mqqqH0e1lhmsEQHV5U8OmdueBC2y0RFr2oUzZoFRtN3MvPmt2fsFRcNQAoGLTSNdHBFUYGnlgcRFhkBbKjPw==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/plugin-syntax-decorators": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.25.7.tgz", + "integrity": "sha512-oXduHo642ZhstLVYTe2z2GSJIruU0c/W3/Ghr6A5yGMsVrvdnxO1z+3pbTcT7f3/Clnt+1z8D/w1r1f1SHaCHw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.7.tgz", + "integrity": "sha512-ZvZQRmME0zfJnDQnVBKYzHxXT7lYBB3Revz1GuS7oLXWMgqUPX4G+DDbT30ICClht9WKV34QVrZhSw6WdklwZQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.7.tgz", + "integrity": "sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.7.tgz", + "integrity": "sha512-ruZOnKO+ajVL/MVx+PwNBPOkrnXTXoWMtte1MBpegfCArhqOe3Bj52avVj1huLLxNKYKXYaSxZ2F+woK1ekXfw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.7.tgz", + "integrity": "sha512-EJN2mKxDwfOUCPxMO6MUI58RN3ganiRAG/MS/S3HfB6QFNjroAMelQo/gybyYq97WerCBAZoyrAoW8Tzdq2jWg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.8.tgz", + "integrity": "sha512-9ypqkozyzpG+HxlH4o4gdctalFGIjjdufzo7I2XPda0iBnZ6a+FO0rIEQcdSPXp02CkvGsII1exJhmROPQd5oA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-remap-async-to-generator": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.7.tgz", + "integrity": "sha512-ZUCjAavsh5CESCmi/xCpX1qcCaAglzs/7tmuvoFnJgA1dM7gQplsguljoTg+Ru8WENpX89cQyAtWoaE0I3X3Pg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-remap-async-to-generator": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.7.tgz", + "integrity": "sha512-xHttvIM9fvqW+0a3tZlYcZYSBpSWzGBFIt/sYG3tcdSzBB8ZeVgz2gBP7Df+sM0N1850jrviYSSeUuc+135dmQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.7.tgz", + "integrity": "sha512-ZEPJSkVZaeTFG/m2PARwLZQ+OG0vFIhPlKHK/JdIMy8DbRJ/htz6LRrTFtdzxi9EHmcwbNPAKDnadpNSIW+Aow==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.7.tgz", + "integrity": "sha512-mhyfEW4gufjIqYFo9krXHJ3ElbFLIze5IDp+wQTxoPd+mwFb1NxatNAwmv8Q8Iuxv7Zc+q8EkiMQwc9IhyGf4g==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.25.8.tgz", + "integrity": "sha512-e82gl3TCorath6YLf9xUwFehVvjvfqFhdOo4+0iVIVju+6XOi5XHkqB3P2AXnSwoeTX0HBoXq5gJFtvotJzFnQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.7.tgz", + "integrity": "sha512-9j9rnl+YCQY0IGoeipXvnk3niWicIB6kCsWRGLwX241qSXpbA4MKxtp/EdvFxsc4zI5vqfLxzOd0twIJ7I99zg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7", + "@babel/traverse": "^7.25.7", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.7.tgz", + "integrity": "sha512-QIv+imtM+EtNxg/XBKL3hiWjgdLjMOmZ+XzQwSgmBfKbfxUjBzGgVPklUuE55eq5/uVoh8gg3dqlrwR/jw3ZeA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/template": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.7.tgz", + "integrity": "sha512-xKcfLTlJYUczdaM1+epcdh1UGewJqr9zATgrNHcLBcV2QmfvPPEixo/sK/syql9cEmbr7ulu5HMFG5vbbt/sEA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.7.tgz", + "integrity": "sha512-kXzXMMRzAtJdDEgQBLF4oaiT6ZCU3oWHgpARnTKDAqPkDJ+bs3NrZb310YYevR5QlRo3Kn7dzzIdHbZm1VzJdQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.7.tgz", + "integrity": "sha512-by+v2CjoL3aMnWDOyCIg+yxU9KXSRa9tN6MbqggH5xvymmr9p4AMjYkNlQy4brMceBnUyHZ9G8RnpvT8wP7Cfg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.7.tgz", + "integrity": "sha512-HvS6JF66xSS5rNKXLqkk7L9c/jZ/cdIVIcoPVrnl8IsVpLggTjXs8OWekbLHs/VtYDDh5WXnQyeE3PPUGm22MA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.8.tgz", + "integrity": "sha512-gznWY+mr4ZQL/EWPcbBQUP3BXS5FwZp8RUOw06BaRn8tQLzN4XLIxXejpHN9Qo8x8jjBmAAKp6FoS51AgkSA/A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.7.tgz", + "integrity": "sha512-yjqtpstPfZ0h/y40fAXRv2snciYr0OAoMXY/0ClC7tm4C/nG5NJKmIItlaYlLbIVAWNfrYuy9dq1bE0SbX0PEg==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.8.tgz", + "integrity": "sha512-sPtYrduWINTQTW7FtOy99VCTWp4H23UX7vYcut7S4CIMEXU+54zKX9uCoGkLsWXteyaMXzVHgzWbLfQ1w4GZgw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.7.tgz", + "integrity": "sha512-n/TaiBGJxYFWvpJDfsxSj9lEEE44BFM1EPGz4KEiTipTgkoFVVcCmzAL3qA7fdQU96dpo4gGf5HBx/KnDvqiHw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.7.tgz", + "integrity": "sha512-5MCTNcjCMxQ63Tdu9rxyN6cAWurqfrDZ76qvVPrGYdBxIj+EawuuxTu/+dgJlhK5eRz3v1gLwp6XwS8XaX2NiQ==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.8.tgz", + "integrity": "sha512-4OMNv7eHTmJ2YXs3tvxAfa/I43di+VcF+M4Wt66c88EAED1RoGaf1D64cL5FkRpNL+Vx9Hds84lksWvd/wMIdA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.7.tgz", + "integrity": "sha512-fwzkLrSu2fESR/cm4t6vqd7ebNIopz2QHGtjoU+dswQo/P6lwAG04Q98lliE3jkz/XqnbGFLnUcE0q0CVUf92w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.8.tgz", + "integrity": "sha512-f5W0AhSbbI+yY6VakT04jmxdxz+WsID0neG7+kQZbCOjuyJNdL5Nn4WIBm4hRpKnUcO9lP0eipUhFN12JpoH8g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.7.tgz", + "integrity": "sha512-Std3kXwpXfRV0QtQy5JJcRpkqP8/wG4XL7hSKZmGlxPlDqmpXtEPRmhF7ztnlTCtUN3eXRUJp+sBEZjaIBVYaw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.7.tgz", + "integrity": "sha512-CgselSGCGzjQvKzghCvDTxKHP3iooenLpJDO842ehn5D2G5fJB222ptnDwQho0WjEvg7zyoxb9P+wiYxiJX5yA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.7.tgz", + "integrity": "sha512-L9Gcahi0kKFYXvweO6n0wc3ZG1ChpSFdgG+eV1WYZ3/dGbJK7vvk91FgGgak8YwRgrCuihF8tE/Xg07EkL5COg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.7.tgz", + "integrity": "sha512-t9jZIvBmOXJsiuyOwhrIGs8dVcD6jDyg2icw1VL4A/g+FnWyJKwUfSSU2nwJuMV2Zqui856El9u+ElB+j9fV1g==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.7.tgz", + "integrity": "sha512-p88Jg6QqsaPh+EB7I9GJrIqi1Zt4ZBHUQtjw3z1bzEXcLh6GfPqzZJ6G+G1HBGKUNukT58MnKG7EN7zXQBCODw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.7.tgz", + "integrity": "sha512-BtAT9LzCISKG3Dsdw5uso4oV1+v2NlVXIIomKJgQybotJY3OwCwJmkongjHgwGKoZXd0qG5UZ12JUlDQ07W6Ow==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.7.tgz", + "integrity": "sha512-CfCS2jDsbcZaVYxRFo2qtavW8SpdzmBXC2LOI4oO0rP+JSRDxxF3inF4GcPsLgfb5FjkhXG5/yR/lxuRs2pySA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.8.tgz", + "integrity": "sha512-Z7WJJWdQc8yCWgAmjI3hyC+5PXIubH9yRKzkl9ZEG647O9szl9zvmKLzpbItlijBnVhTUf1cpyWBsZ3+2wjWPQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.8.tgz", + "integrity": "sha512-rm9a5iEFPS4iMIy+/A/PiS0QN0UyjPIeVvbU5EMZFKJZHt8vQnasbpo3T3EFcxzCeYO0BHfc4RqooCZc51J86Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.8.tgz", + "integrity": "sha512-LkUu0O2hnUKHKE7/zYOIjByMa4VRaV2CD/cdGz0AxU9we+VA3kDDggKEzI0Oz1IroG+6gUP6UmWEHBMWZU316g==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/plugin-transform-parameters": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.7.tgz", + "integrity": "sha512-pWT6UXCEW3u1t2tcAGtE15ornCBvopHj9Bps9D2DsH15APgNVOTwwczGckX+WkAvBmuoYKRCFa4DK+jM8vh5AA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.8.tgz", + "integrity": "sha512-EbQYweoMAHOn7iJ9GgZo14ghhb9tTjgOc88xFgYngifx7Z9u580cENCV159M4xDh3q/irbhSjZVpuhpC2gKBbg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.8.tgz", + "integrity": "sha512-q05Bk7gXOxpTHoQ8RSzGSh/LHVB9JEIkKnk3myAWwZHnYiTGYtbdrYkIsS8Xyh4ltKf7GNUSgzs/6P2bJtBAQg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.7.tgz", + "integrity": "sha512-FYiTvku63me9+1Nz7TOx4YMtW3tWXzfANZtrzHhUZrz4d47EEtMQhzFoZWESfXuAMMT5mwzD4+y1N8ONAX6lMQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.7.tgz", + "integrity": "sha512-KY0hh2FluNxMLwOCHbxVOKfdB5sjWG4M183885FmaqWWiGMhRZq4DQRKH6mHdEucbJnyDyYiZNwNG424RymJjA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.8.tgz", + "integrity": "sha512-8Uh966svuB4V8RHHg0QJOB32QK287NBksJOByoKmHMp1TAobNniNalIkI2i5IPj5+S9NYCG4VIjbEuiSN8r+ow==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.7.tgz", + "integrity": "sha512-lQEeetGKfFi0wHbt8ClQrUSUMfEeI3MMm74Z73T9/kuz990yYVtfofjf3NuA42Jy3auFOpbjDyCSiIkTs1VIYw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.7.tgz", + "integrity": "sha512-mgDoQCRjrY3XK95UuV60tZlFCQGXEtMg8H+IsW72ldw1ih1jZhzYXbJvghmAEpg5UVhhnCeia1CkGttUvCkiMQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.7.tgz", + "integrity": "sha512-3OfyfRRqiGeOvIWSagcwUTVk2hXBsr/ww7bLn6TRTuXnexA+Udov2icFOxFX9abaj4l96ooYkcNN1qi2Zvqwng==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.25.7.tgz", + "integrity": "sha512-Y9p487tyTzB0yDYQOtWnC+9HGOuogtP3/wNpun1xJXEEvI6vip59BSBTsHnekZLqxmPcgsrAKt46HAAb//xGhg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.7.tgz", + "integrity": "sha512-uBbxNwimHi5Bv3hUccmOFlUy3ATO6WagTApenHz9KzoIdn0XeACdB12ZJ4cjhuB2WSi80Ez2FWzJnarccriJeA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.7.tgz", + "integrity": "sha512-Mm6aeymI0PBh44xNIv/qvo8nmbkpZze1KvR8MkEqbIREDxoiWTi18Zr2jryfRMwDfVZF9foKh060fWgni44luw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.7.tgz", + "integrity": "sha512-ZFAeNkpGuLnAQ/NCsXJ6xik7Id+tHuS+NT+ue/2+rn/31zcdnupCdmunOizEaP0JsUmTFSTOPoQY7PkK2pttXw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.7.tgz", + "integrity": "sha512-SI274k0nUsFFmyQupiO7+wKATAmMFf8iFgq2O+vVFXZ0SV9lNfT1NGzBEhjquFmD8I9sqHLguH+gZVN3vww2AA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.7.tgz", + "integrity": "sha512-OmWmQtTHnO8RSUbL0NTdtpbZHeNTnm68Gj5pA4Y2blFNh+V4iZR68V1qL9cI37J21ZN7AaCnkfdHtLExQPf2uA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.7.tgz", + "integrity": "sha512-BN87D7KpbdiABA+t3HbVqHzKWUDN3dymLaTnPFAMyc8lV+KN3+YzNhVRNdinaCPA4AUqx7ubXbQ9shRjYBl3SQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.7.tgz", + "integrity": "sha512-IWfR89zcEPQGB/iB408uGtSPlQd3Jpq11Im86vUgcmSTcoWAiQMCTOa2K2yNNqFJEBVICKhayctee65Ka8OB0w==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.7.tgz", + "integrity": "sha512-8JKfg/hiuA3qXnlLx8qtv5HWRbgyFx2hMMtpDDuU2rTckpKkGu4ycK5yYHwuEa16/quXfoxHBIApEsNyMWnt0g==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.7.tgz", + "integrity": "sha512-YRW8o9vzImwmh4Q3Rffd09bH5/hvY0pxg+1H1i0f7APoUeg12G7+HhLj9ZFNIrYkgBXhIijPJ+IXypN0hLTIbw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.8.tgz", + "integrity": "sha512-58T2yulDHMN8YMUxiLq5YmWUnlDCyY1FsHM+v12VMx+1/FlrUj5tY50iDCpofFQEM8fMYOaY9YRvym2jcjn1Dg==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.25.8", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.25.7", + "@babel/plugin-syntax-import-attributes": "^7.25.7", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.25.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.8", + "@babel/plugin-transform-async-to-generator": "^7.25.7", + "@babel/plugin-transform-block-scoped-functions": "^7.25.7", + "@babel/plugin-transform-block-scoping": "^7.25.7", + "@babel/plugin-transform-class-properties": "^7.25.7", + "@babel/plugin-transform-class-static-block": "^7.25.8", + "@babel/plugin-transform-classes": "^7.25.7", + "@babel/plugin-transform-computed-properties": "^7.25.7", + "@babel/plugin-transform-destructuring": "^7.25.7", + "@babel/plugin-transform-dotall-regex": "^7.25.7", + "@babel/plugin-transform-duplicate-keys": "^7.25.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.7", + "@babel/plugin-transform-dynamic-import": "^7.25.8", + "@babel/plugin-transform-exponentiation-operator": "^7.25.7", + "@babel/plugin-transform-export-namespace-from": "^7.25.8", + "@babel/plugin-transform-for-of": "^7.25.7", + "@babel/plugin-transform-function-name": "^7.25.7", + "@babel/plugin-transform-json-strings": "^7.25.8", + "@babel/plugin-transform-literals": "^7.25.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.8", + "@babel/plugin-transform-member-expression-literals": "^7.25.7", + "@babel/plugin-transform-modules-amd": "^7.25.7", + "@babel/plugin-transform-modules-commonjs": "^7.25.7", + "@babel/plugin-transform-modules-systemjs": "^7.25.7", + "@babel/plugin-transform-modules-umd": "^7.25.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.7", + "@babel/plugin-transform-new-target": "^7.25.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.8", + "@babel/plugin-transform-numeric-separator": "^7.25.8", + "@babel/plugin-transform-object-rest-spread": "^7.25.8", + "@babel/plugin-transform-object-super": "^7.25.7", + "@babel/plugin-transform-optional-catch-binding": "^7.25.8", + "@babel/plugin-transform-optional-chaining": "^7.25.8", + "@babel/plugin-transform-parameters": "^7.25.7", + "@babel/plugin-transform-private-methods": "^7.25.7", + "@babel/plugin-transform-private-property-in-object": "^7.25.8", + "@babel/plugin-transform-property-literals": "^7.25.7", + "@babel/plugin-transform-regenerator": "^7.25.7", + "@babel/plugin-transform-reserved-words": "^7.25.7", + "@babel/plugin-transform-shorthand-properties": "^7.25.7", + "@babel/plugin-transform-spread": "^7.25.7", + "@babel/plugin-transform-sticky-regex": "^7.25.7", + "@babel/plugin-transform-template-literals": "^7.25.7", + "@babel/plugin-transform-typeof-symbol": "^7.25.7", + "@babel/plugin-transform-unicode-escapes": "^7.25.7", + "@babel/plugin-transform-unicode-property-regex": "^7.25.7", + "@babel/plugin-transform-unicode-regex": "^7.25.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.38.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", + "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", + "dependencies": { + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cnakazawa/watch": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", + "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", + "dev": true, + "dependencies": { + "exec-sh": "^0.3.2", + "minimist": "^1.2.0" + }, + "bin": { + "watch": "cli.js" + }, + "engines": { + "node": ">=0.1.95" + } + }, + "node_modules/@hapi/address": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", + "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==", + "deprecated": "Moved to 'npm install @sideway/address'", + "dev": true + }, + "node_modules/@hapi/bourne": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-1.3.2.tgz", + "integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==", + "deprecated": "This version has been deprecated and is no longer supported or maintained", + "dev": true + }, + "node_modules/@hapi/hoek": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz", + "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==", + "deprecated": "This version has been deprecated and is no longer supported or maintained", + "dev": true + }, + "node_modules/@hapi/joi": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.1.tgz", + "integrity": "sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==", + "deprecated": "Switch to 'npm install joi'", + "dev": true, + "dependencies": { + "@hapi/address": "2.x.x", + "@hapi/bourne": "1.x.x", + "@hapi/hoek": "8.x.x", + "@hapi/topo": "3.x.x" + } + }, + "node_modules/@hapi/topo": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz", + "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==", + "deprecated": "This version has been deprecated and is no longer supported or maintained", + "dev": true, + "dependencies": { + "@hapi/hoek": "^8.3.0" + } + }, + "node_modules/@intervolga/optimize-cssnano-plugin": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@intervolga/optimize-cssnano-plugin/-/optimize-cssnano-plugin-1.0.6.tgz", + "integrity": "sha512-zN69TnSr0viRSU6cEDIcuPcP67QcpQ6uHACg58FiN9PDrU6SLyGW3MR4tiISbYxy1kDWAVPwD+XwQTWE5cigAA==", + "dev": true, + "dependencies": { + "cssnano": "^4.0.0", + "cssnano-preset-default": "^4.0.0", + "postcss": "^7.0.0" + }, + "peerDependencies": { + "webpack": "^4.0.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jest/console": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz", + "integrity": "sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ==", + "dev": true, + "dependencies": { + "@jest/source-map": "^24.9.0", + "chalk": "^2.0.1", + "slash": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/core": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-24.9.0.tgz", + "integrity": "sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A==", + "dev": true, + "dependencies": { + "@jest/console": "^24.7.1", + "@jest/reporters": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "graceful-fs": "^4.1.15", + "jest-changed-files": "^24.9.0", + "jest-config": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.9.0", + "jest-resolve-dependencies": "^24.9.0", + "jest-runner": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "jest-watcher": "^24.9.0", + "micromatch": "^3.1.10", + "p-each-series": "^1.0.0", + "realpath-native": "^1.1.0", + "rimraf": "^2.5.4", + "slash": "^2.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/core/node_modules/ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@jest/core/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@jest/environment": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-24.9.0.tgz", + "integrity": "sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/fake-timers": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-24.9.0.tgz", + "integrity": "sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-mock": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/reporters": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-24.9.0.tgz", + "integrity": "sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw==", + "dev": true, + "dependencies": { + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "glob": "^7.1.2", + "istanbul-lib-coverage": "^2.0.2", + "istanbul-lib-instrument": "^3.0.1", + "istanbul-lib-report": "^2.0.4", + "istanbul-lib-source-maps": "^3.0.1", + "istanbul-reports": "^2.2.6", + "jest-haste-map": "^24.9.0", + "jest-resolve": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-util": "^24.9.0", + "jest-worker": "^24.6.0", + "node-notifier": "^5.4.2", + "slash": "^2.0.0", + "source-map": "^0.6.0", + "string-length": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/source-map": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-24.9.0.tgz", + "integrity": "sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0", + "graceful-fs": "^4.1.15", + "source-map": "^0.6.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/source-map/node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@jest/test-result": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-24.9.0.tgz", + "integrity": "sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA==", + "dev": true, + "dependencies": { + "@jest/console": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/istanbul-lib-coverage": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz", + "integrity": "sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A==", + "dev": true, + "dependencies": { + "@jest/test-result": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-runner": "^24.9.0", + "jest-runtime": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/transform": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-24.9.0.tgz", + "integrity": "sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.1.0", + "@jest/types": "^24.9.0", + "babel-plugin-istanbul": "^5.1.0", + "chalk": "^2.0.1", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.1.15", + "jest-haste-map": "^24.9.0", + "jest-regex-util": "^24.9.0", + "jest-util": "^24.9.0", + "micromatch": "^3.1.10", + "pirates": "^4.0.1", + "realpath-native": "^1.1.0", + "slash": "^2.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "2.4.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jest/transform/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mdi/js": { + "version": "7.3.67", + "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.3.67.tgz", + "integrity": "sha512-MnRjknFqpTC6FifhGHjZ0+QYq2bAkZFQqIj8JA2AdPZbBxUvr8QSgB2yPAJ8/ob/XkR41xlg5majDR3c1JP1hw==" + }, + "node_modules/@mrmlnc/readdir-enhanced": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", + "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", + "dev": true, + "dependencies": { + "call-me-maybe": "^1.0.1", + "glob-to-regexp": "^0.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@node-ipc/js-queue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@node-ipc/js-queue/-/js-queue-2.0.3.tgz", + "integrity": "sha512-fL1wpr8hhD5gT2dA1qifeVaoDFlQR5es8tFuKqjHX+kdOtdNHnxkVZbtIrR2rxnMFvehkjaZRNV2H/gPXlb0hw==", + "dev": true, + "dependencies": { + "easy-stack": "1.0.1" + }, + "engines": { + "node": ">=1.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.scandir/node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", + "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@soda/friendly-errors-webpack-plugin": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.8.1.tgz", + "integrity": "sha512-h2ooWqP8XuFqTXT+NyAFbrArzfQA7R6HTezADrvD9Re8fxMLTPPniLdqVTdDaO0eIoLaAwKT+d6w+5GeTk7Vbg==", + "dev": true, + "dependencies": { + "chalk": "^3.0.0", + "error-stack-parser": "^2.0.6", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/@soda/friendly-errors-webpack-plugin/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@soda/friendly-errors-webpack-plugin/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@soda/friendly-errors-webpack-plugin/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@soda/friendly-errors-webpack-plugin/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@soda/friendly-errors-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@soda/friendly-errors-webpack-plugin/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@soda/get-current-script": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@soda/get-current-script/-/get-current-script-1.0.2.tgz", + "integrity": "sha512-T7VNNlYVM1SgQ+VsMYhnDkcGmWhQdL0bDyGm5TlQ3GBXnJscEClUUOKduWTmm2zCnvNLC1hc3JpuXjs/nFOc5w==", + "dev": true + }, + "node_modules/@stylelint/postcss-css-in-js": { + "version": "0.37.3", + "resolved": "https://registry.npmjs.org/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.3.tgz", + "integrity": "sha512-scLk3cSH1H9KggSniseb2KNAU5D9FWc3H7BxCSAIdtU9OWIyw0zkEZ9qEKHryRM+SExYXRKNb7tOOVNAsQ3iwg==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "dependencies": { + "@babel/core": "^7.17.9" + }, + "peerDependencies": { + "postcss": ">=7.0.0", + "postcss-syntax": ">=0.36.2" + } + }, + "node_modules/@stylelint/postcss-markdown": { + "version": "0.36.2", + "resolved": "https://registry.npmjs.org/@stylelint/postcss-markdown/-/postcss-markdown-0.36.2.tgz", + "integrity": "sha512-2kGbqUVJUGE8dM+bMzXG/PYUWKkjLIkRLWNh39OaADkiabDRdw8ATFCgbMz5xdIcvwspPAluSL7uY+ZiTWdWmQ==", + "deprecated": "Use the original unforked package instead: postcss-markdown", + "dev": true, + "dependencies": { + "remark": "^13.0.0", + "unist-util-find-all-after": "^3.0.2" + }, + "peerDependencies": { + "postcss": ">=7.0.0", + "postcss-syntax": ">=0.36.2" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.0.tgz", + "integrity": "sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.9.1.tgz", + "integrity": "sha512-Fb38HkXSVA4L8fGKEZ6le5bB8r6MRWlOCZbVuWZcmOMSCd2wCYOwN1ibj8daIoV9naq7aaOZjrLCoCMptKU/4Q==", + "dev": true, + "dependencies": { + "jest-diff": "^24.3.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "dev": true, + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true + }, + "node_modules/@types/node": { + "version": "22.7.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.7.tgz", + "integrity": "sha512-SRxCrrg9CL/y54aiMCG3edPKdprgMVGDXjA3gB8UmmBW5TcXzRUYAh8EWzTnSJFAd1rgImPELza+A3bJ+qxz8Q==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true + }, + "node_modules/@types/q": { + "version": "1.5.8", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz", + "integrity": "sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz", + "integrity": "sha512-7i+zxXdPD0T4cKDuxCUXJ4wHcsJLwENa6Z3dCu8cfCK743OGy5Nu1RmAGqDPsoTDINVEcdXKRvR/zre+P2Ku1A==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/source-list-map": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.6.tgz", + "integrity": "sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", + "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", + "dev": true + }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true + }, + "node_modules/@types/stylelint": { + "version": "13.13.3", + "resolved": "https://registry.npmjs.org/@types/stylelint/-/stylelint-13.13.3.tgz", + "integrity": "sha512-xvYwobi9L69FXbJTimKYRNHyMwtmcJxMd1woI3U822rkW/f7wcZ6fsV1DqYPT+sNaO0qUtngiBhTQfMeItUvUA==", + "dev": true, + "dependencies": { + "globby": "11.x.x", + "postcss": "7.x.x" + } + }, + "node_modules/@types/stylelint/node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/stylelint/node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@types/stylelint/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@types/stylelint/node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@types/stylelint/node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@types/stylelint/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@types/stylelint/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/stylelint/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/stylelint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@types/stylelint/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@types/stylelint/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@types/stylelint/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@types/stylelint/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@types/stylelint/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/@types/tapable": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.12.tgz", + "integrity": "sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q==", + "dev": true + }, + "node_modules/@types/uglify-js": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.5.tgz", + "integrity": "sha512-TU+fZFBTBcXj/GpDpDaBmgWk/gn96kMZ+uocaFUlV2f8a6WdMzzI44QBCmGcCiYR0Y6ZlNRiyUyKKt5nl/lbzQ==", + "dev": true, + "dependencies": { + "source-map": "^0.6.1" + } + }, + "node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "dev": true + }, + "node_modules/@types/webpack": { + "version": "4.41.39", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.39.tgz", + "integrity": "sha512-otxUJvoi6FbBq/64gGH34eblpKLgdi+gf08GaAh8Bx6So0ZZic028Ev/SUxD22gbthMKCkeeiXEat1kHLDJfYg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tapable": "^1", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "anymatch": "^3.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/@types/webpack-dev-server": { + "version": "3.11.6", + "resolved": "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.11.6.tgz", + "integrity": "sha512-XCph0RiiqFGetukCTC3KVnY1jwLcZ84illFRMbyFzCcWl90B/76ew0tSqF46oBhnLC4obNDG7dMO0JfTN0MgMQ==", + "dev": true, + "dependencies": { + "@types/connect-history-api-fallback": "*", + "@types/express": "*", + "@types/serve-static": "*", + "@types/webpack": "^4", + "http-proxy-middleware": "^1.0.0" + } + }, + "node_modules/@types/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-4nZOdMwSPHZ4pTEZzSp0AsTM4K7Qmu40UKW4tJDiOVs20UzYF9l+qUe4s0ftfN0pin06n+5cWWDJXH+sbhAiDw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.7.3" + } + }, + "node_modules/@types/webpack-sources/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/yargs": { + "version": "13.0.12", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.12.tgz", + "integrity": "sha512-qCxJE1qgz2y0hA4pIxjBR+PelCH0U5CK1XJXFwCNqfmliatKp47UCXXE9Dyk1OXBDLvsCF57TqQEJaeLfDYEOQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@vue/babel-helper-vue-jsx-merge-props": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz", + "integrity": "sha512-JkqXfCkUDp4PIlFdDQ0TdXoIejMtTHP67/pvxlgeY+u5k3LEdKuWZ3LK6xkxo52uDoABIVyRwqVkfLQJhk7VBA==", + "dev": true + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.2.5.tgz", + "integrity": "sha512-lOz4t39ZdmU4DJAa2hwPYmKc8EsuGa2U0L9KaZaOJUt0UwQNjNA3AZTq6uEivhOKhhG1Wvy96SvYBoFmCg3uuw==", + "dev": true + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.2.5.tgz", + "integrity": "sha512-zTrNmOd4939H9KsRIGmmzn3q2zvv1mjxkYZHgqHZgDrXz5B1Q3WyGEjO2f+JrmKghvl1JIRcvo63LgM1kH5zFg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.6", + "@babel/types": "^7.25.6", + "@vue/babel-helper-vue-transform-on": "1.2.5", + "@vue/babel-plugin-resolve-type": "1.2.5", + "html-tags": "^3.3.1", + "svg-tags": "^1.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.2.5.tgz", + "integrity": "sha512-U/ibkQrf5sx0XXRnUZD1mo5F7PkpKyTbfXM3a3rC4YnUz6crHEz9Jg09jzzL6QYlXNto/9CePdOg/c87O4Nlfg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/parser": "^7.25.6", + "@vue/compiler-sfc": "^3.5.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-plugin-transform-vue-jsx": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.4.0.tgz", + "integrity": "sha512-Fmastxw4MMx0vlgLS4XBX0XiBbUFzoMGeVXuMV08wyOfXdikAFqBTuYPR0tlk+XskL19EzHc39SgjrPGY23JnA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.2.0", + "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", + "html-tags": "^2.0.0", + "lodash.kebabcase": "^4.1.1", + "svg-tags": "^1.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-plugin-transform-vue-jsx/node_modules/html-tags": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz", + "integrity": "sha512-+Il6N8cCo2wB/Vd3gqy/8TZhTD3QvcVeQLCnZiGkGCH3JP28IgGAY41giccp2W4R3jfyJPAP318FQTa1yU7K7g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vue/babel-preset-app": { + "version": "4.5.19", + "resolved": "https://registry.npmjs.org/@vue/babel-preset-app/-/babel-preset-app-4.5.19.tgz", + "integrity": "sha512-VCNRiAt2P/bLo09rYt3DLe6xXUMlhJwrvU18Ddd/lYJgC7s8+wvhgYs+MTx4OiAXdu58drGwSBO9SPx7C6J82Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.0", + "@babel/helper-compilation-targets": "^7.9.6", + "@babel/helper-module-imports": "^7.8.3", + "@babel/plugin-proposal-class-properties": "^7.8.3", + "@babel/plugin-proposal-decorators": "^7.8.3", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-jsx": "^7.8.3", + "@babel/plugin-transform-runtime": "^7.11.0", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.0", + "@vue/babel-plugin-jsx": "^1.0.3", + "@vue/babel-preset-jsx": "^1.2.4", + "babel-plugin-dynamic-import-node": "^2.3.3", + "core-js": "^3.6.5", + "core-js-compat": "^3.6.5", + "semver": "^6.1.0" + }, + "peerDependencies": { + "@babel/core": "*", + "core-js": "^3", + "vue": "^2 || ^3.0.0-0" + }, + "peerDependenciesMeta": { + "core-js": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@vue/babel-preset-jsx": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-preset-jsx/-/babel-preset-jsx-1.4.0.tgz", + "integrity": "sha512-QmfRpssBOPZWL5xw7fOuHNifCQcNQC1PrOo/4fu6xlhlKJJKSA3HqX92Nvgyx8fqHZTUGMPHmFA+IDqwXlqkSA==", + "dev": true, + "dependencies": { + "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", + "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", + "@vue/babel-sugar-composition-api-inject-h": "^1.4.0", + "@vue/babel-sugar-composition-api-render-instance": "^1.4.0", + "@vue/babel-sugar-functional-vue": "^1.4.0", + "@vue/babel-sugar-inject-h": "^1.4.0", + "@vue/babel-sugar-v-model": "^1.4.0", + "@vue/babel-sugar-v-on": "^1.4.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0", + "vue": "*" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, + "node_modules/@vue/babel-sugar-composition-api-inject-h": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-composition-api-inject-h/-/babel-sugar-composition-api-inject-h-1.4.0.tgz", + "integrity": "sha512-VQq6zEddJHctnG4w3TfmlVp5FzDavUSut/DwR0xVoe/mJKXyMcsIibL42wPntozITEoY90aBV0/1d2KjxHU52g==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-jsx": "^7.2.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-sugar-composition-api-render-instance": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-composition-api-render-instance/-/babel-sugar-composition-api-render-instance-1.4.0.tgz", + "integrity": "sha512-6ZDAzcxvy7VcnCjNdHJ59mwK02ZFuP5CnucloidqlZwVQv5CQLijc3lGpR7MD3TWFi78J7+a8J56YxbCtHgT9Q==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-jsx": "^7.2.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-sugar-functional-vue": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.4.0.tgz", + "integrity": "sha512-lTEB4WUFNzYt2In6JsoF9sAYVTo84wC4e+PoZWSgM6FUtqRJz7wMylaEhSRgG71YF+wfLD6cc9nqVeXN2rwBvw==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-jsx": "^7.2.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-sugar-inject-h": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.4.0.tgz", + "integrity": "sha512-muwWrPKli77uO2fFM7eA3G1lAGnERuSz2NgAxuOLzrsTlQl8W4G+wwbM4nB6iewlKbwKRae3nL03UaF5ffAPMA==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-jsx": "^7.2.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-sugar-v-model": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.4.0.tgz", + "integrity": "sha512-0t4HGgXb7WHYLBciZzN5s0Hzqan4Ue+p/3FdQdcaHAb7s5D9WZFGoSxEZHrR1TFVZlAPu1bejTKGeAzaaG3NCQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-jsx": "^7.2.0", + "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", + "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", + "camelcase": "^5.0.0", + "html-tags": "^2.0.0", + "svg-tags": "^1.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-sugar-v-model/node_modules/html-tags": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz", + "integrity": "sha512-+Il6N8cCo2wB/Vd3gqy/8TZhTD3QvcVeQLCnZiGkGCH3JP28IgGAY41giccp2W4R3jfyJPAP318FQTa1yU7K7g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@vue/babel-sugar-v-on": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.4.0.tgz", + "integrity": "sha512-m+zud4wKLzSKgQrWwhqRObWzmTuyzl6vOP7024lrpeJM4x2UhQtRDLgYjXAw9xBXjCwS0pP9kXjg91F9ZNo9JA==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-jsx": "^7.2.0", + "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", + "camelcase": "^5.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/cli-overlay": { + "version": "4.5.19", + "resolved": "https://registry.npmjs.org/@vue/cli-overlay/-/cli-overlay-4.5.19.tgz", + "integrity": "sha512-GdxvNSmOw7NHIazCO8gTK+xZbaOmScTtxj6eHVeMbYpDYVPJ+th3VMLWNpw/b6uOjwzzcyKlA5dRQ1DAb+gF/g==", + "dev": true + }, + "node_modules/@vue/cli-plugin-babel": { + "version": "4.5.19", + "resolved": "https://registry.npmjs.org/@vue/cli-plugin-babel/-/cli-plugin-babel-4.5.19.tgz", + "integrity": "sha512-8ebXzaMW9KNTMAN6+DzkhFsjty1ieqT7hIW5Lbk4v30Qhfjkms7lBWyXPGkoq+wAikXFa1Gnam2xmWOBqDDvWg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.0", + "@vue/babel-preset-app": "^4.5.19", + "@vue/cli-shared-utils": "^4.5.19", + "babel-loader": "^8.1.0", + "cache-loader": "^4.1.0", + "thread-loader": "^2.1.3", + "webpack": "^4.0.0" + }, + "peerDependencies": { + "@vue/cli-service": "^3.0.0 || ^4.0.0-0" + } + }, + "node_modules/@vue/cli-plugin-eslint": { + "version": "4.5.19", + "resolved": "https://registry.npmjs.org/@vue/cli-plugin-eslint/-/cli-plugin-eslint-4.5.19.tgz", + "integrity": "sha512-53sa4Pu9j5KajesFlj494CcO8vVo3e3nnZ1CCKjGGnrF90id1rUeepcFfz5XjwfEtbJZp2x/NoX/EZE6zCzSFQ==", + "dev": true, + "dependencies": { + "@vue/cli-shared-utils": "^4.5.19", + "eslint-loader": "^2.2.1", + "globby": "^9.2.0", + "inquirer": "^7.1.0", + "webpack": "^4.0.0", + "yorkie": "^2.0.0" + }, + "peerDependencies": { + "@vue/cli-service": "^3.0.0 || ^4.0.0-0", + "eslint": ">= 1.6.0 < 7.0.0" + } + }, + "node_modules/@vue/cli-plugin-router": { + "version": "4.5.19", + "resolved": "https://registry.npmjs.org/@vue/cli-plugin-router/-/cli-plugin-router-4.5.19.tgz", + "integrity": "sha512-3icGzH1IbVYmMMsOwYa0lal/gtvZLebFXdE5hcQJo2mnTwngXGMTyYAzL56EgHBPjbMmRpyj6Iw9k4aVInVX6A==", + "dev": true, + "dependencies": { + "@vue/cli-shared-utils": "^4.5.19" + }, + "peerDependencies": { + "@vue/cli-service": "^3.0.0 || ^4.0.0-0" + } + }, + "node_modules/@vue/cli-plugin-unit-jest": { + "version": "4.5.19", + "resolved": "https://registry.npmjs.org/@vue/cli-plugin-unit-jest/-/cli-plugin-unit-jest-4.5.19.tgz", + "integrity": "sha512-yX61mpeU7DnjOv+Lxtjmr3pzESqBLIXeTK4MJpa/UdzrhnylHP4r6mCYETNLEYtxp8WZUXPjZFIzrKn5poZPJg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.0", + "@babel/plugin-transform-modules-commonjs": "^7.9.6", + "@types/jest": "^24.0.19", + "@vue/cli-shared-utils": "^4.5.19", + "babel-core": "^7.0.0-bridge.0", + "babel-jest": "^24.9.0", + "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", + "deepmerge": "^4.2.2", + "jest": "^24.9.0", + "jest-environment-jsdom-fifteen": "^1.0.2", + "jest-serializer-vue": "^2.0.2", + "jest-transform-stub": "^2.0.0", + "jest-watch-typeahead": "^0.4.2", + "ts-jest": "^24.2.0", + "vue-jest": "^3.0.5" + }, + "peerDependencies": { + "@vue/cli-service": "^3.0.0 || ^4.0.0-0" + } + }, + "node_modules/@vue/cli-plugin-vuex": { + "version": "4.5.19", + "resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.19.tgz", + "integrity": "sha512-DUmfdkG3pCdkP7Iznd87RfE9Qm42mgp2hcrNcYQYSru1W1gX2dG/JcW8bxmeGSa06lsxi9LEIc/QD1yPajSCZw==", + "dev": true, + "peerDependencies": { + "@vue/cli-service": "^3.0.0 || ^4.0.0-0" + } + }, + "node_modules/@vue/cli-service": { + "version": "4.5.19", + "resolved": "https://registry.npmjs.org/@vue/cli-service/-/cli-service-4.5.19.tgz", + "integrity": "sha512-+Wpvj8fMTCt9ZPOLu5YaLkFCQmB4MrZ26aRmhhKiCQ/4PMoL6mLezfqdt6c+m2htM+1WV5RunRo+0WHl2DfwZA==", + "dev": true, + "dependencies": { + "@intervolga/optimize-cssnano-plugin": "^1.0.5", + "@soda/friendly-errors-webpack-plugin": "^1.7.1", + "@soda/get-current-script": "^1.0.0", + "@types/minimist": "^1.2.0", + "@types/webpack": "^4.0.0", + "@types/webpack-dev-server": "^3.11.0", + "@vue/cli-overlay": "^4.5.19", + "@vue/cli-plugin-router": "^4.5.19", + "@vue/cli-plugin-vuex": "^4.5.19", + "@vue/cli-shared-utils": "^4.5.19", + "@vue/component-compiler-utils": "^3.1.2", + "@vue/preload-webpack-plugin": "^1.1.0", + "@vue/web-component-wrapper": "^1.2.0", + "acorn": "^7.4.0", + "acorn-walk": "^7.1.1", + "address": "^1.1.2", + "autoprefixer": "^9.8.6", + "browserslist": "^4.12.0", + "cache-loader": "^4.1.0", + "case-sensitive-paths-webpack-plugin": "^2.3.0", + "cli-highlight": "^2.1.4", + "clipboardy": "^2.3.0", + "cliui": "^6.0.0", + "copy-webpack-plugin": "^5.1.1", + "css-loader": "^3.5.3", + "cssnano": "^4.1.10", + "debug": "^4.1.1", + "default-gateway": "^5.0.5", + "dotenv": "^8.2.0", + "dotenv-expand": "^5.1.0", + "file-loader": "^4.2.0", + "fs-extra": "^7.0.1", + "globby": "^9.2.0", + "hash-sum": "^2.0.0", + "html-webpack-plugin": "^3.2.0", + "launch-editor-middleware": "^2.2.1", + "lodash.defaultsdeep": "^4.6.1", + "lodash.mapvalues": "^4.6.0", + "lodash.transform": "^4.6.0", + "mini-css-extract-plugin": "^0.9.0", + "minimist": "^1.2.5", + "pnp-webpack-plugin": "^1.6.4", + "portfinder": "^1.0.26", + "postcss-loader": "^3.0.0", + "ssri": "^8.0.1", + "terser-webpack-plugin": "^1.4.4", + "thread-loader": "^2.1.3", + "url-loader": "^2.2.0", + "vue-loader": "^15.9.2", + "vue-style-loader": "^4.1.2", + "webpack": "^4.0.0", + "webpack-bundle-analyzer": "^3.8.0", + "webpack-chain": "^6.4.0", + "webpack-dev-server": "^3.11.0", + "webpack-merge": "^4.2.2" + }, + "bin": { + "vue-cli-service": "bin/vue-cli-service.js" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "vue-loader-v16": "npm:vue-loader@^16.1.0" + }, + "peerDependencies": { + "@vue/compiler-sfc": "^3.0.0-beta.14", + "vue-template-compiler": "^2.0.0" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + }, + "less-loader": { + "optional": true + }, + "pug-plain-loader": { + "optional": true + }, + "raw-loader": { + "optional": true + }, + "sass-loader": { + "optional": true + }, + "stylus-loader": { + "optional": true + }, + "vue-template-compiler": { + "optional": true + } + } + }, + "node_modules/@vue/cli-shared-utils": { + "version": "4.5.19", + "resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.19.tgz", + "integrity": "sha512-JYpdsrC/d9elerKxbEUtmSSU6QRM60rirVubOewECHkBHj+tLNznWq/EhCjswywtePyLaMUK25eTqnTSZlEE+g==", + "dev": true, + "dependencies": { + "@achrinza/node-ipc": "9.2.2", + "@hapi/joi": "^15.0.1", + "chalk": "^2.4.2", + "execa": "^1.0.0", + "launch-editor": "^2.2.1", + "lru-cache": "^5.1.1", + "open": "^6.3.0", + "ora": "^3.4.0", + "read-pkg": "^5.1.1", + "request": "^2.88.2", + "semver": "^6.1.0", + "strip-ansi": "^6.0.0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.12.tgz", + "integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.12", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz", + "integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==", + "dev": true, + "dependencies": { + "@vue/compiler-core": "3.5.12", + "@vue/shared": "3.5.12" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz", + "integrity": "sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.12", + "@vue/compiler-dom": "3.5.12", + "@vue/compiler-ssr": "3.5.12", + "@vue/shared": "3.5.12", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.47", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-sfc/node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz", + "integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.5.12", + "@vue/shared": "3.5.12" + } + }, + "node_modules/@vue/component-compiler-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.3.0.tgz", + "integrity": "sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==", + "dev": true, + "dependencies": { + "consolidate": "^0.15.1", + "hash-sum": "^1.0.2", + "lru-cache": "^4.1.2", + "merge-source-map": "^1.1.0", + "postcss": "^7.0.36", + "postcss-selector-parser": "^6.0.2", + "source-map": "~0.6.1", + "vue-template-es2015-compiler": "^1.9.0" + }, + "optionalDependencies": { + "prettier": "^1.18.2 || ^2.0.0" + } + }, + "node_modules/@vue/component-compiler-utils/node_modules/hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", + "dev": true + }, + "node_modules/@vue/component-compiler-utils/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/@vue/component-compiler-utils/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + }, + "node_modules/@vue/eslint-config-airbnb": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-airbnb/-/eslint-config-airbnb-5.3.0.tgz", + "integrity": "sha512-m9ldRhbqaODbcc9mQZjPgnTzyNweZblLMTqMfC2kHWY68dYd3kwG/hvENeZWXJnKKo+eGnoptk+7Zq/c1519ZQ==", + "dev": true, + "dependencies": { + "eslint-config-airbnb-base": "^14.0.0", + "eslint-import-resolver-node": "^0.3.4", + "eslint-import-resolver-webpack": "^0.13.0", + "eslint-plugin-import": "^2.21.2" + }, + "peerDependencies": { + "@vue/cli-service": "^3.0.0 || ^4.0.0-0 || ^5.0.0-0", + "eslint": "^5.16.0 || ^6.1.0 || ^7.2.0", + "eslint-plugin-import": "^2.18.2" + } + }, + "node_modules/@vue/preload-webpack-plugin": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.2.tgz", + "integrity": "sha512-LIZMuJk38pk9U9Ur4YzHjlIyMuxPlACdBIHH9/nGYVTsaGKOSnSuELiE8vS9wa+dJpIYspYUOqk+L1Q4pgHQHQ==", + "dev": true, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "html-webpack-plugin": ">=2.26.0", + "webpack": ">=4.0.0" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.12.tgz", + "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==", + "dev": true + }, + "node_modules/@vue/test-utils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-1.3.6.tgz", + "integrity": "sha512-udMmmF1ts3zwxUJEIAj5ziioR900reDrt6C9H3XpWPsLBx2lpHKoA4BTdd9HNIYbkGltWw+JjWJ+5O6QBwiyEw==", + "dev": true, + "dependencies": { + "dom-event-types": "^1.0.0", + "lodash": "^4.17.15", + "pretty": "^2.0.0" + }, + "peerDependencies": { + "vue": "2.x", + "vue-template-compiler": "^2.x" + } + }, + "node_modules/@vue/web-component-wrapper": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz", + "integrity": "sha512-Iu8Tbg3f+emIIMmI2ycSI8QcEuAUgPTgHwesDU1eKMLE4YC/c/sFbGc70QgMq31ijRftV0R7vCm9co6rldCeOA==", + "dev": true + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", + "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", + "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", + "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", + "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-code-frame": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", + "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", + "dev": true, + "dependencies": { + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "node_modules/@webassemblyjs/helper-fsm": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", + "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-module-context": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", + "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.9.0" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", + "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", + "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", + "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", + "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", + "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", + "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/helper-wasm-section": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-opt": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "@webassemblyjs/wast-printer": "1.9.0" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", + "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", + "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", + "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" + } + }, + "node_modules/@webassemblyjs/wast-parser": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", + "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/floating-point-hex-parser": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-code-frame": "1.9.0", + "@webassemblyjs/helper-fsm": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", + "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", + "dev": true, + "dependencies": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + } + }, + "node_modules/acorn-globals/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals/node_modules/acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/address": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", + "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", + "dev": true, + "peerDependencies": { + "ajv": ">=5.0.0" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/alphanum-sort": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", + "integrity": "sha512-0FcBfdcmaumGPQ0qPn7Q5qTgz/ooXgIyp1rf8ik5bGX8mpE2YHjC0P/eyQvxu1GURYQgq9ozf2mteQ5ZD9YiyQ==", + "dev": true + }, + "node_modules/ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-equal": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.2.tgz", + "integrity": "sha512-gUHx76KtnhEgB3HOuFYiCm3FIdEs6ocM2asHvNTkfu/Y09qQVrrVVaOKENmS2KkSaGoxgXNqC+ZVtR/n0MOkSA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.reduce": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.7.tgz", + "integrity": "sha512-mzmiUCVwtiD4lgxYP8g7IYy8El8p2CSMePvIbTS7gchKir/L1fgJrk0yDKmAX6mnRQFKNADYIk8nNlTris5H1Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-array-method-boxes-properly": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + }, + "node_modules/assert": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.1.tgz", + "integrity": "sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==", + "dev": true, + "dependencies": { + "object.assign": "^4.1.4", + "util": "^0.10.4" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assert/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/assert/node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dev": true, + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/async-each": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz", + "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true, + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/autoprefixer": { + "version": "9.8.8", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.8.tgz", + "integrity": "sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==", + "dev": true, + "dependencies": { + "browserslist": "^4.12.0", + "caniuse-lite": "^1.0.30001109", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "picocolors": "^0.2.1", + "postcss": "^7.0.32", + "postcss-value-parser": "^4.1.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + }, + "node_modules/autoprefixer/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true + }, + "node_modules/axios": { + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==", + "dev": true, + "dependencies": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "node_modules/babel-code-frame/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==", + "dev": true + }, + "node_modules/babel-code-frame/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-code-frame/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/babel-core": { + "version": "7.0.0-bridge.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", + "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", + "dev": true, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-eslint": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", + "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", + "deprecated": "babel-eslint is now @babel/eslint-parser. This package will no longer receive updates.", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "@babel/parser": "^7.7.0", + "@babel/traverse": "^7.7.0", + "@babel/types": "^7.7.0", + "eslint-visitor-keys": "^1.0.0", + "resolve": "^1.12.0" + }, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "eslint": ">= 4.12.1" + } + }, + "node_modules/babel-jest": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.9.0.tgz", + "integrity": "sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw==", + "dev": true, + "dependencies": { + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/babel__core": "^7.1.0", + "babel-plugin-istanbul": "^5.1.0", + "babel-preset-jest": "^24.9.0", + "chalk": "^2.4.2", + "slash": "^2.0.0" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-loader": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.4.1.tgz", + "integrity": "sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA==", + "dev": true, + "dependencies": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^2.0.4", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "engines": { + "node": ">= 8.9" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "webpack": ">=2" + } + }, + "node_modules/babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha512-Bl3ZiA+LjqaMtNYopA9TYE9HP1tQ+E5dLxE0XrAzcIJeK2UqF0/EaqXwBn9esd4UmTfEab+P+UYQ1GnioFIb/w==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0" + } + }, + "node_modules/babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "dev": true, + "dependencies": { + "object.assign": "^4.1.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz", + "integrity": "sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "find-up": "^3.0.0", + "istanbul-lib-instrument": "^3.3.0", + "test-exclude": "^5.2.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.9.0.tgz", + "integrity": "sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw==", + "dev": true, + "dependencies": { + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/babel-plugin-lodash": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/babel-plugin-lodash/-/babel-plugin-lodash-3.3.4.tgz", + "integrity": "sha512-yDZLjK7TCkWl1gpBeBGmuaDIFhZKmkoL+Cu2MUUjv5VxUZx/z7tBGBCBcQs5RI1Bkz5LLmNdjx7paOyQtMovyg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.0.0-beta.49", + "@babel/types": "^7.0.0-beta.49", + "glob": "^7.1.1", + "lodash": "^4.17.10", + "require-package-name": "^2.0.1" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", + "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", + "dev": true, + "dependencies": { + "babel-plugin-transform-strict-mode": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-types": "^6.26.0" + } + }, + "node_modules/babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha512-j3KtSpjyLSJxNoCDrhwiJad8kw0gJ9REGj8/CqL0HeRyLnvUNYV9zcqluL6QJSXh3nfsLEmSLvwRfGzrgR96Pw==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "node_modules/babel-preset-jest": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz", + "integrity": "sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-object-rest-spread": "^7.0.0", + "babel-plugin-jest-hoist": "^24.9.0" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "dev": true, + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "node_modules/babel-runtime/node_modules/core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "dev": true, + "hasInstallScript": true + }, + "node_modules/babel-runtime/node_modules/regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "node_modules/babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha512-PCOcLFW7/eazGUKIoqH97sO9A2UYMahsn/yRQ7uOk37iutwjq7ODtcTNF+iFDSHNfkctqsLRjLP7URnOx0T1fg==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "node_modules/babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha512-iSxeXx7apsjCHe9c7n8VtRXGzI2Bk1rBSOJgCCjfyXb6v1aCqE1KSEpq/8SXuVN8Ka/Rh1WDTF0MDzkvTA4MIA==", + "dev": true, + "dependencies": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "node_modules/babel-traverse/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/babel-traverse/node_modules/globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babel-traverse/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha512-zhe3V/26rCWsEZK8kZN+HaQj5yQ1CilTObixFzKW1UWjqG7618Twz6YEsCnjfg5gBcJh02DrpCkS9h98ZqDY+g==", + "dev": true, + "dependencies": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "node_modules/babel-types/node_modules/to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha512-lxrWP8ejsq+7E3nNjwYmUBMAgjMTZoTI+sdBOpvNyijeDLa29LUn9QaoXAHv4+Z578hbmHHJKZknzxVtvo77og==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true, + "bin": { + "babylon": "bin/babylon.js" + } + }, + "node_modules/bail": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", + "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "dependencies": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bfj": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.2.tgz", + "integrity": "sha512-BmBJa4Lip6BPRINSZ0BPEIfB1wUY/9rwbwvIHQA1KjX9om29B6id0wnWXq7m3bn5JrUVjeOTnVuhPT1FiHwPGw==", + "dev": true, + "dependencies": { + "bluebird": "^3.5.5", + "check-types": "^8.0.3", + "hoopy": "^0.1.4", + "tryer": "^1.0.1" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dev": true + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha512-RaVTblr+OnEli0r/ud8InrU7D+G0y6aJhlxaLa6Pwty4+xoxboF1BsUI45tujvRpbj9dQVoglChqonGAsjEBYg==", + "dev": true, + "dependencies": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "node_modules/bonjour/node_modules/array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/braces/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "dev": true + }, + "node_modules/browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "dev": true + }, + "node_modules/browser-resolve": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "dev": true, + "dependencies": { + "resolve": "1.1.7" + } + }, + "node_modules/browser-resolve/node_modules/resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==", + "dev": true + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", + "dev": true, + "dependencies": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-rsa/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/browserify-sign": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", + "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", + "dev": true, + "dependencies": { + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.5", + "hash-base": "~3.0", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.7", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/browserify-sign/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "dependencies": { + "pako": "~1.0.5" + } + }, + "node_modules/browserslist": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, + "node_modules/buffer-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-json/-/buffer-json-2.0.0.tgz", + "integrity": "sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw==", + "dev": true + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", + "dev": true + }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", + "dev": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", + "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", + "dev": true, + "dependencies": { + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" + } + }, + "node_modules/cacache/node_modules/ssri": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", + "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", + "dev": true, + "dependencies": { + "figgy-pudding": "^3.5.1" + } + }, + "node_modules/cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "dependencies": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cache-loader": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cache-loader/-/cache-loader-4.1.0.tgz", + "integrity": "sha512-ftOayxve0PwKzBF/GLsZNC9fJBXl8lkZE3TOsjkboHfVHVkL39iUEs1FO07A33mizmci5Dudt38UZrrYXDtbhw==", + "dev": true, + "dependencies": { + "buffer-json": "^2.0.0", + "find-cache-dir": "^3.0.0", + "loader-utils": "^1.2.3", + "mkdirp": "^0.5.1", + "neo-async": "^2.6.1", + "schema-utils": "^2.0.0" + }, + "engines": { + "node": ">= 8.9.0" + }, + "peerDependencies": { + "webpack": "^4.0.0" + } + }, + "node_modules/cache-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/cache-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "dev": true + }, + "node_modules/caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", + "dev": true, + "dependencies": { + "callsites": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", + "dev": true, + "dependencies": { + "caller-callsite": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==", + "dev": true, + "dependencies": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001669", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz", + "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/capture-exit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", + "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "dev": true, + "dependencies": { + "rsvp": "^4.8.4" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/case-sensitive-paths-webpack-plugin": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", + "integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/check-types": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz", + "integrity": "sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==", + "dev": true + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chokidar/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/chokidar/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "node_modules/cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/clean-css": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", + "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cli-highlight/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cli-highlight/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-highlight/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cli-highlight/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/cli-highlight/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-highlight/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/clipboardy": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz", + "integrity": "sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ==", + "dev": true, + "dependencies": { + "arch": "^2.1.1", + "execa": "^1.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-regexp": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz", + "integrity": "sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==", + "dev": true, + "dependencies": { + "is-regexp": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "dev": true, + "dependencies": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "dev": true, + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "dev": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/condense-newlines": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/condense-newlines/-/condense-newlines-0.2.1.tgz", + "integrity": "sha512-P7X+QL9Hb9B/c8HI5BFFKmjgBu2XpQuF98WZ9XkO+dBGgk5XgwiQz7o1SmpglNWId3581UcS0SFAWfoIhMHPfg==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-whitespace": "^0.3.0", + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/condense-newlines/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/condense-newlines/node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/condense-newlines/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/condense-newlines/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true + }, + "node_modules/connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, + "node_modules/consolidate": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", + "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", + "deprecated": "Please upgrade to consolidate v1.0.0+ as it has been modernized with several long-awaited fixes implemented. Maintenance is supported by Forward Email at https://forwardemail.net ; follow/watch https://github.com/ladjs/consolidate for updates and release changelog", + "dev": true, + "dependencies": { + "bluebird": "^3.1.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "node_modules/copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "node_modules/copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.1.2.tgz", + "integrity": "sha512-Uh7crJAco3AjBvgAy9Z75CjK8IG+gxaErro71THQ+vv/bl4HaQcpkexAY8KVW/T6D2W2IRr+couF/knIRkZMIQ==", + "dev": true, + "dependencies": { + "cacache": "^12.0.3", + "find-cache-dir": "^2.1.0", + "glob-parent": "^3.1.0", + "globby": "^7.1.1", + "is-glob": "^4.0.1", + "loader-utils": "^1.2.3", + "minimatch": "^3.0.4", + "normalize-path": "^3.0.0", + "p-limit": "^2.2.1", + "schema-utils": "^1.0.0", + "serialize-javascript": "^4.0.0", + "webpack-log": "^2.0.0" + }, + "engines": { + "node": ">= 6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", + "integrity": "sha512-yANWAN2DUcBtuus5Cpd+SKROzXHs2iVXFZt/Ykrfz6SAXqacLX25NZpltE+39ceMexYF4TtEadjuSTw8+3wX4g==", + "dev": true, + "dependencies": { + "array-union": "^1.0.1", + "dir-glob": "^2.0.0", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "node_modules/copy-webpack-plugin/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/copy-webpack-plugin/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/copy-webpack-plugin/node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/copy-webpack-plugin/node_modules/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/copy-webpack-plugin/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/core-js": { + "version": "3.23.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.23.5.tgz", + "integrity": "sha512-7Vh11tujtAZy82da4duVreQysIoO2EvVrur7y6IzZkH1IHPSekuDi8Vuw1+YKjkbfWLRD7Nc9ICQ/sIUDutcyg==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", + "integrity": "sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "dev": true, + "dependencies": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "dev": true, + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "node_modules/create-ecdh/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "dependencies": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + }, + "engines": { + "node": "*" + } + }, + "node_modules/css": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", + "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "source-map": "^0.6.1", + "source-map-resolve": "^0.5.2", + "urix": "^0.1.0" + } + }, + "node_modules/css-color-names": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "integrity": "sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/css-declaration-sorter": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", + "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", + "dev": true, + "dependencies": { + "postcss": "^7.0.1", + "timsort": "^0.3.0" + }, + "engines": { + "node": ">4" + } + }, + "node_modules/css-loader": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz", + "integrity": "sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "cssesc": "^3.0.0", + "icss-utils": "^4.1.1", + "loader-utils": "^1.2.3", + "normalize-path": "^3.0.0", + "postcss": "^7.0.32", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^3.0.2", + "postcss-modules-scope": "^2.2.0", + "postcss-modules-values": "^3.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^2.7.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/css-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/css-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", + "dev": true + }, + "node_modules/css-tree": { + "version": "1.0.0-alpha.37", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", + "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.4", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.11.tgz", + "integrity": "sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g==", + "dev": true, + "dependencies": { + "cosmiconfig": "^5.0.0", + "cssnano-preset-default": "^4.0.8", + "is-resolvable": "^1.0.0", + "postcss": "^7.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/cssnano-preset-default": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz", + "integrity": "sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==", + "dev": true, + "dependencies": { + "css-declaration-sorter": "^4.0.1", + "cssnano-util-raw-cache": "^4.0.1", + "postcss": "^7.0.0", + "postcss-calc": "^7.0.1", + "postcss-colormin": "^4.0.3", + "postcss-convert-values": "^4.0.1", + "postcss-discard-comments": "^4.0.2", + "postcss-discard-duplicates": "^4.0.2", + "postcss-discard-empty": "^4.0.1", + "postcss-discard-overridden": "^4.0.1", + "postcss-merge-longhand": "^4.0.11", + "postcss-merge-rules": "^4.0.3", + "postcss-minify-font-values": "^4.0.2", + "postcss-minify-gradients": "^4.0.2", + "postcss-minify-params": "^4.0.2", + "postcss-minify-selectors": "^4.0.2", + "postcss-normalize-charset": "^4.0.1", + "postcss-normalize-display-values": "^4.0.2", + "postcss-normalize-positions": "^4.0.2", + "postcss-normalize-repeat-style": "^4.0.2", + "postcss-normalize-string": "^4.0.2", + "postcss-normalize-timing-functions": "^4.0.2", + "postcss-normalize-unicode": "^4.0.1", + "postcss-normalize-url": "^4.0.1", + "postcss-normalize-whitespace": "^4.0.2", + "postcss-ordered-values": "^4.1.2", + "postcss-reduce-initial": "^4.0.3", + "postcss-reduce-transforms": "^4.0.2", + "postcss-svgo": "^4.0.3", + "postcss-unique-selectors": "^4.0.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/cssnano-util-get-arguments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", + "integrity": "sha512-6RIcwmV3/cBMG8Aj5gucQRsJb4vv4I4rn6YjPbVWd5+Pn/fuG+YseGvXGk00XLkoZkaj31QOD7vMUpNPC4FIuw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/cssnano-util-get-match": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", + "integrity": "sha512-JPMZ1TSMRUPVIqEalIBNoBtAYbi8okvcFns4O0YIhcdGebeYZK7dMyHJiQ6GqNBA9kE0Hym4Aqym5rPdsV/4Cw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/cssnano-util-raw-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", + "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/cssnano-util-same-parent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", + "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dev": true, + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true + }, + "node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "node_modules/cssstyle": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz", + "integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==", + "dev": true, + "dependencies": { + "cssom": "0.3.x" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/cyclist": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.2.tgz", + "integrity": "sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==", + "dev": true + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "node_modules/deasync": { + "version": "0.1.30", + "resolved": "https://registry.npmjs.org/deasync/-/deasync-0.1.30.tgz", + "integrity": "sha512-OaAjvEQuQ9tJsKG4oHO9nV1UHTwb2Qc2+fadB0VeVtD0Z9wiG1XPGLJ4W3aLhAoQSYTaLROFRbd5X20Dkzf7MQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^1.7.1" + }, + "engines": { + "node": ">=0.11.0" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decache": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/decache/-/decache-4.6.2.tgz", + "integrity": "sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==", + "dev": true, + "dependencies": { + "callsite": "^1.0.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/deep-equal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz", + "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==", + "dev": true, + "dependencies": { + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.5.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-5.0.5.tgz", + "integrity": "sha512-z2RnruVmj8hVMmAnEJMTIJNijhKCDiGjbLP+BHJFOT7ld3Bo5qcIBpVYDniqhbMIIf+jZDlkP2MkPXiQy/DBLA==", + "dev": true, + "dependencies": { + "execa": "^3.3.0" + }, + "engines": { + "node": "^8.12.0 || >=9.7.0" + } + }, + "node_modules/default-gateway/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/default-gateway/node_modules/execa": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz", + "integrity": "sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "p-finally": "^2.0.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": "^8.12.0 || >=9.7.0" + } + }, + "node_modules/default-gateway/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-gateway/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-gateway/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/default-gateway/node_modules/p-finally": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", + "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/default-gateway/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/default-gateway/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/default-gateway/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/default-gateway/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults/node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", + "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", + "dev": true, + "dependencies": { + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/del/node_modules/globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "dev": true, + "dependencies": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del/node_modules/globby/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha512-CwffZFvlJffUg9zZA0uqrjQayUTC8ob94pnr5sFwaVv3IOmkfUHcWH+jXaQK3askE51Cqe8/9Ql/0uXNwqZ8Zg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/diff-sequences": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", + "integrity": "sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", + "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", + "dev": true, + "dependencies": { + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", + "dev": true + }, + "node_modules/dns-packet": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz", + "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==", + "dev": true, + "dependencies": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha512-Ix5PrWjphuSoUXV/Zv5gaFHjnaJtb02F2+Si3Ht9dyJ87+Z/lMmy+dpNHtTGraNK958ndXq2i+GLkWsWHcKaBQ==", + "dev": true, + "dependencies": { + "buffer-indexof": "^1.0.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-event-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/dom-event-types/-/dom-event-types-1.1.0.tgz", + "integrity": "sha512-jNCX+uNJ3v38BKvPbpki6j5ItVlnSqVV6vDWGS6rExzCMjsc39frLjm1n91o6YaKK6AZl0wLloItW6C6mr61BQ==", + "dev": true + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true, + "engines": { + "node": ">=0.4", + "npm": ">=1.2" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "dependencies": { + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv-expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", + "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", + "dev": true + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/easy-stack": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz", + "integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/editorconfig/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/ejs": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz", + "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==", + "dev": true, + "hasInstallScript": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.41", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.41.tgz", + "integrity": "sha512-dfdv/2xNjX0P8Vzme4cfzHqnPm5xsZXwsolTYr0eyW18IUmNyG08vL+fttvinTfhKfIKdRoqkDIC9e9iWQCNYQ==", + "dev": true + }, + "node_modules/elliptic": { + "version": "6.5.7", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz", + "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==", + "dev": true, + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz", + "integrity": "sha512-kxpoMgrdtkXZ5h0SeraBS1iRntpTpQ3R8ussdb38+UAFnMGX5DDyJXePm+OCHOcoXvHDw7mc2erbJBpDnl7TPw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.2.0", + "tapable": "^0.1.8" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dev": true, + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", + "dev": true + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^8.10.0 || ^10.13.0 || >=11.10.1" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-airbnb-base": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.1.tgz", + "integrity": "sha512-GOrQyDtVEc1Xy20U7vsB2yAoB4nBlfH5HZJeatRXHleO+OS5Ot+MWij4Dpltw4/DyIkqUfqz1epfhVR5XWWQPA==", + "dev": true, + "dependencies": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.2" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "eslint": "^5.16.0 || ^6.8.0 || ^7.2.0", + "eslint-plugin-import": "^2.22.1" + } + }, + "node_modules/eslint-config-prettier": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz", + "integrity": "sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw==", + "dev": true, + "dependencies": { + "get-stdin": "^6.0.0" + }, + "bin": { + "eslint-config-prettier-check": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=3.14.1" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-webpack": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.13.9.tgz", + "integrity": "sha512-yGngeefNiHXau2yzKKs2BNON4HLpxBabY40BGL/vUSKZtqzjlVsTTZm57jhHULhm+mJEwKsEIIN3NXup5AiiBQ==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "enhanced-resolve": "^0.9.1", + "find-root": "^1.1.0", + "hasown": "^2.0.0", + "interpret": "^1.4.0", + "is-core-module": "^2.13.1", + "is-regex": "^1.1.4", + "lodash": "^4.17.21", + "resolve": "^2.0.0-next.5", + "semver": "^5.7.2" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "eslint-plugin-import": ">=1.4.0", + "webpack": ">=1.11.0" + } + }, + "node_modules/eslint-import-resolver-webpack/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-webpack/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-import-resolver-webpack/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/eslint-loader": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/eslint-loader/-/eslint-loader-2.2.1.tgz", + "integrity": "sha512-RLgV9hoCVsMLvOxCuNjdqOrUqIj9oJg8hF44vzJaYqsAHuY9G2YAeN3joQ9nxP0p5Th9iFSIpKo+SD8KISxXRg==", + "deprecated": "This loader has been deprecated. Please use eslint-webpack-plugin", + "dev": true, + "dependencies": { + "loader-fs-cache": "^1.0.0", + "loader-utils": "^1.0.2", + "object-assign": "^4.0.1", + "object-hash": "^1.1.4", + "rimraf": "^2.6.1" + }, + "peerDependencies": { + "eslint": ">=1.6.0 <7.0.0", + "webpack": ">=2.0.0 <5.0.0" + } + }, + "node_modules/eslint-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/eslint-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.25.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz", + "integrity": "sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.2", + "has": "^1.0.3", + "is-core-module": "^2.8.0", + "is-glob": "^4.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.5", + "resolve": "^1.20.0", + "tsconfig-paths": "^3.12.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/eslint-plugin-prettier": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz", + "integrity": "sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "eslint": ">=5.0.0", + "prettier": ">=1.13.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-8.5.0.tgz", + "integrity": "sha512-i1uHCTAKOoEj12RDvdtONWrGzjFm/djkzqfhmQ0d6M/W8KM81mhswd/z+iTZ0jCpdUedW3YRgcVfQ37/J4zoYQ==", + "dev": true, + "dependencies": { + "eslint-utils": "^3.0.0", + "natural-compare": "^1.4.0", + "semver": "^7.3.5", + "vue-eslint-parser": "^8.0.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-vue/node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-plugin-vue/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-vue/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "dependencies": { + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "dependencies": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-pubsub": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz", + "integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/exec-sh": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz", + "integrity": "sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==", + "dev": true + }, + "node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/execall": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/execall/-/execall-2.0.0.tgz", + "integrity": "sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==", + "dev": true, + "dependencies": { + "clone-regexp": "^2.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "dev": true, + "dependencies": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/expand-brackets/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/expect": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-24.9.0.tgz", + "integrity": "sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "ansi-styles": "^3.2.0", + "jest-get-type": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-regex-util": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/express": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.10", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/express/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/express/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "dependencies": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extract-from-css": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/extract-from-css/-/extract-from-css-0.4.4.tgz", + "integrity": "sha512-41qWGBdtKp9U7sgBxAQ7vonYqSXzgW/SiAYzq4tdWSVhAShvpVCH1nyvPQgjse6EdgbW7Y7ERdT3674/lKr65A==", + "dev": true, + "dependencies": { + "css": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0", + "npm": ">=2.0.0" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", + "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", + "dev": true, + "dependencies": { + "@mrmlnc/readdir-enhanced": "^2.2.1", + "@nodelib/fs.stat": "^1.1.2", + "glob-parent": "^3.1.0", + "is-glob": "^4.0.0", + "merge2": "^1.2.3", + "micromatch": "^3.1.10" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-uri": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/figgy-pudding": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", + "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", + "deprecated": "This module is no longer supported.", + "dev": true + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "dependencies": { + "flat-cache": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/file-loader": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-4.3.0.tgz", + "integrity": "sha512-aKrYPYjF1yG3oX0kWRrqrSMfgftm7oJW5M+m4owoldH5C51C0RkIwB++JbRvEW3IU6/ZG5n8UvEcdgwOt2UOWA==", + "dev": true, + "dependencies": { + "loader-utils": "^1.2.3", + "schema-utils": "^2.5.0" + }, + "engines": { + "node": ">= 8.9.0" + }, + "peerDependencies": { + "webpack": "^4.0.0" + } + }, + "node_modules/file-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/file-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true + }, + "node_modules/filesize": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", + "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fill-range/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/find-babel-config": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/find-babel-config/-/find-babel-config-1.2.2.tgz", + "integrity": "sha512-oK59njMyw2y3yxto1BCfVK7MQp/OYf4FleHu0RgosH3riFJ1aOuo/7naLDLAObfrgn3ueFhw5sAT/cp0QuJI3Q==", + "dev": true, + "dependencies": { + "json5": "^1.0.2", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/find-babel-config/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true + }, + "node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "dependencies": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "node_modules/flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/foreground-child/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "dev": true, + "dependencies": { + "map-cache": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha512-gehEzmPn2nAwr39eay+x3X34Ra+M2QlVUTLhkXPjWdeO8RF9kszk116avgBJM3ZyNHgHXBNx+VmPaFC36k0PzA==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "deprecated": "Upgrade to fsevents v2 to mitigate potential security issues", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "dev": true, + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/glob-parent/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", + "integrity": "sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig==", + "dev": true + }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz", + "integrity": "sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==", + "dev": true, + "dependencies": { + "@types/glob": "^7.1.1", + "array-union": "^1.0.2", + "dir-glob": "^2.2.2", + "fast-glob": "^2.2.6", + "glob": "^7.1.3", + "ignore": "^4.0.3", + "pify": "^4.0.1", + "slash": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", + "dev": true + }, + "node_modules/gonzales-pe": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz", + "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "gonzales": "bin/gonzales.js" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==", + "dev": true + }, + "node_modules/gzip-size": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", + "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.1", + "pify": "^4.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dev": true, + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/has": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "dev": true, + "dependencies": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha512-EeeoJKjTyt868liAlVmcv2ZsUfGHlE3Q+BICOXcZiwN3osr5Q/zFGYmTJpoIzuaSTAwndFy+GqhEwlU4L3j4Ow==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "dev": true + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hex-color-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", + "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", + "dev": true + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dev": true, + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/hoopy": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", + "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", + "dev": true, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hsl-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", + "integrity": "sha512-M5ezZw4LzXbBKMruP+BNANf0k+19hDQMgpzBIYnya//Al+fjNct9Wf3b1WedLqdEs2hKBvxq/jh+DsHJLj0F9A==", + "dev": true + }, + "node_modules/hsla-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", + "integrity": "sha512-7Wn5GMLuHBjZCb2bTmnDOycho0p/7UVaAeqXZGbHrBCl6Yd/xDhQJAXe6Ga9AXJH2I5zY1dEdYw2u1UptnSBJA==", + "dev": true + }, + "node_modules/html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^1.0.1" + } + }, + "node_modules/html-entities": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", + "integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==", + "dev": true + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/html-minifier": { + "version": "3.5.21", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", + "integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==", + "dev": true, + "dependencies": { + "camel-case": "3.0.x", + "clean-css": "4.2.x", + "commander": "2.17.x", + "he": "1.2.x", + "param-case": "2.1.x", + "relateurl": "0.2.x", + "uglify-js": "3.4.x" + }, + "bin": { + "html-minifier": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/html-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "integrity": "sha512-Br4ifmjQojUP4EmHnRBoUIYcZ9J7M4bTMcm7u6xoIAIuq2Nte4TzXX0533owvkQKQD1WeMTTTyD4Ni4QKxS0Bg==", + "deprecated": "3.x is no longer supported", + "dev": true, + "dependencies": { + "html-minifier": "^3.2.3", + "loader-utils": "^0.2.16", + "lodash": "^4.17.3", + "pretty-error": "^2.0.2", + "tapable": "^1.0.0", + "toposort": "^1.0.0", + "util.promisify": "1.0.0" + }, + "engines": { + "node": ">=6.9" + }, + "peerDependencies": { + "webpack": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/html-webpack-plugin/node_modules/big.js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", + "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/html-webpack-plugin/node_modules/emojis-list": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", + "integrity": "sha512-knHEZMgs8BB+MInokmNTg/OyPlAddghe1YBgNwJBc5zsJi/uyIcXoSDsL/W9ymOsBoBGdPIHXYJ9+qKFwRwDng==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/html-webpack-plugin/node_modules/json5": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/html-webpack-plugin/node_modules/loader-utils": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", + "integrity": "sha512-tiv66G0SmiOx+pLWMtGEkfSEejxvb6N6uRrQjfWJIT79W9GMpgKeCAmm9aVBKtd4WEgntciI8CsGqjpDoCWJug==", + "dev": true, + "dependencies": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0", + "object-assign": "^4.0.1" + } + }, + "node_modules/html-webpack-plugin/node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.3.1.tgz", + "integrity": "sha512-13eVVDYS4z79w7f1+NPllJtOQFx/FdUW4btIvVRMaRlUY9VGstAbo5MOhLEuUgZFRHn3x50ufn25zkj/boZnEg==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.5", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/http-proxy-middleware/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/http-proxy-middleware/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/http-proxy-middleware/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/http-proxy-middleware/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", + "dev": true + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", + "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", + "dev": true, + "dependencies": { + "postcss": "^7.0.14" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==", + "dev": true + }, + "node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", + "integrity": "sha512-Ew5AZzJQFqrOV5BTW3EIoHAnoie1LojZLXKcCQ/yTRyVZosBhK1x1ViYjHGf5pAFOq8ZyChZp6m/fSN7pJyZtg==", + "dev": true, + "dependencies": { + "import-from": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", + "dev": true, + "dependencies": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-from": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", + "integrity": "sha512-0vdnLL2wSGnhlRmzHJAg5JHjt1l2vYhzJ7tNLGbeVg0fse56tpGaH0uzH+r9Slej+BSXXEHvBKDEnVSLLE9/+w==", + "dev": true, + "dependencies": { + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-local": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", + "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", + "dev": true, + "dependencies": { + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/import-local/node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA==", + "dev": true + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/inquirer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/inquirer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/internal-ip": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", + "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", + "dev": true, + "dependencies": { + "default-gateway": "^4.2.0", + "ipaddr.js": "^1.9.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/internal-ip/node_modules/default-gateway": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", + "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", + "dev": true, + "dependencies": { + "execa": "^1.0.0", + "ip-regex": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/ip": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.9.tgz", + "integrity": "sha512-cyRxvOEpNHNtchU3Ln9KC/auJgup87llfQpQ+t5ghoC/UhL16SWzbueiCsdTnWmqAWl7LadfuwhlqmtOaqMHdQ==", + "dev": true + }, + "node_modules/ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-absolute-url": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", + "integrity": "sha512-vOx7VprsKyllwjSkLV79NIhpyLfr3jAp7VaTCMXOJHu4m0Ew1CZ2fcjASwmV1jI3BWuWHB013M48eyeldk9gYg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-accessor-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", + "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dev": true, + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-color-stop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", + "integrity": "sha512-H1U8Vz0cfXNujrJzEcvvwMDW9Ra+biSYA3ThdQvAnMLJkEHQXn6bWzLkxHtVYJ+Sdbx0b6finn3jZiaVe7MAHA==", + "dev": true, + "dependencies": { + "css-color-names": "^0.0.4", + "hex-color-regex": "^1.1.0", + "hsl-regex": "^1.0.0", + "hsla-regex": "^1.0.0", + "rgb-regex": "^1.0.1", + "rgba-regex": "^1.0.0" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", + "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-descriptor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number/node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-in-cwd": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", + "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "dev": true, + "dependencies": { + "is-path-inside": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", + "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "dev": true, + "dependencies": { + "path-is-inside": "^1.0.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regexp": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-2.1.0.tgz", + "integrity": "sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-whitespace": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-whitespace/-/is-whitespace-0.3.0.tgz", + "integrity": "sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "dependencies": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-report/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/istanbul-reports": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz", + "integrity": "sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/javascript-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz", + "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==", + "dev": true + }, + "node_modules/jest": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-24.9.0.tgz", + "integrity": "sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw==", + "dev": true, + "dependencies": { + "import-local": "^2.0.0", + "jest-cli": "^24.9.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-changed-files": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-24.9.0.tgz", + "integrity": "sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "execa": "^1.0.0", + "throat": "^4.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-cli": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-24.9.0.tgz", + "integrity": "sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg==", + "dev": true, + "dependencies": { + "@jest/core": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "import-local": "^2.0.0", + "is-ci": "^2.0.0", + "jest-config": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "prompts": "^2.0.1", + "realpath-native": "^1.1.0", + "yargs": "^13.3.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-cli/node_modules/cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/jest-cli/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "node_modules/jest-cli/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-cli/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-cli/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-cli/node_modules/wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-cli/node_modules/yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "dependencies": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "node_modules/jest-cli/node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/jest-config": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-24.9.0.tgz", + "integrity": "sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^24.9.0", + "@jest/types": "^24.9.0", + "babel-jest": "^24.9.0", + "chalk": "^2.0.1", + "glob": "^7.1.1", + "jest-environment-jsdom": "^24.9.0", + "jest-environment-node": "^24.9.0", + "jest-get-type": "^24.9.0", + "jest-jasmine2": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "micromatch": "^3.1.10", + "pretty-format": "^24.9.0", + "realpath-native": "^1.1.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-diff": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz", + "integrity": "sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==", + "dev": true, + "dependencies": { + "chalk": "^2.0.1", + "diff-sequences": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-docblock": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-24.9.0.tgz", + "integrity": "sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA==", + "dev": true, + "dependencies": { + "detect-newline": "^2.1.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-each": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-24.9.0.tgz", + "integrity": "sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "jest-get-type": "^24.9.0", + "jest-util": "^24.9.0", + "pretty-format": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz", + "integrity": "sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA==", + "dev": true, + "dependencies": { + "@jest/environment": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-util": "^24.9.0", + "jsdom": "^11.5.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-environment-jsdom-fifteen": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom-fifteen/-/jest-environment-jsdom-fifteen-1.0.2.tgz", + "integrity": "sha512-nfrnAfwklE1872LIB31HcjM65cWTh1wzvMSp10IYtPJjLDUbTTvDpajZgIxUnhRmzGvogdHDayCIlerLK0OBBg==", + "dev": true, + "dependencies": { + "@jest/environment": "^24.3.0", + "@jest/fake-timers": "^24.3.0", + "@jest/types": "^24.3.0", + "jest-mock": "^24.0.0", + "jest-util": "^24.0.0", + "jsdom": "^15.2.1" + } + }, + "node_modules/jest-environment-jsdom-fifteen/node_modules/cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "dev": true + }, + "node_modules/jest-environment-jsdom-fifteen/node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom-fifteen/node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "node_modules/jest-environment-jsdom-fifteen/node_modules/jsdom": { + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", + "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", + "dev": true, + "dependencies": { + "abab": "^2.0.0", + "acorn": "^7.1.0", + "acorn-globals": "^4.3.2", + "array-equal": "^1.0.0", + "cssom": "^0.4.1", + "cssstyle": "^2.0.0", + "data-urls": "^1.1.0", + "domexception": "^1.0.1", + "escodegen": "^1.11.1", + "html-encoding-sniffer": "^1.0.2", + "nwsapi": "^2.2.0", + "parse5": "5.1.0", + "pn": "^1.1.0", + "request": "^2.88.0", + "request-promise-native": "^1.0.7", + "saxes": "^3.1.9", + "symbol-tree": "^3.2.2", + "tough-cookie": "^3.0.1", + "w3c-hr-time": "^1.0.1", + "w3c-xmlserializer": "^1.1.2", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^7.0.0", + "ws": "^7.0.0", + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom-fifteen/node_modules/parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", + "dev": true + }, + "node_modules/jest-environment-jsdom-fifteen/node_modules/tough-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "dev": true, + "dependencies": { + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-environment-jsdom-fifteen/node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/jest-environment-jsdom-fifteen/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/jest-environment-node": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-24.9.0.tgz", + "integrity": "sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA==", + "dev": true, + "dependencies": { + "@jest/environment": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-util": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-get-type": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-haste-map": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-24.9.0.tgz", + "integrity": "sha512-kfVFmsuWui2Sj1Rp1AJ4D9HqJwE4uwTlS/vO+eRUaMmd54BFpli2XhMQnPC2k4cHFVbB2Q2C+jtI1AGLgEnCjQ==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "anymatch": "^2.0.0", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.1.15", + "invariant": "^2.2.4", + "jest-serializer": "^24.9.0", + "jest-util": "^24.9.0", + "jest-worker": "^24.9.0", + "micromatch": "^3.1.10", + "sane": "^4.0.3", + "walker": "^1.0.7" + }, + "engines": { + "node": ">= 6" + }, + "optionalDependencies": { + "fsevents": "^1.2.7" + } + }, + "node_modules/jest-haste-map/node_modules/anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "dependencies": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "node_modules/jest-haste-map/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-jasmine2": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-24.9.0.tgz", + "integrity": "sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "co": "^4.6.0", + "expect": "^24.9.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "pretty-format": "^24.9.0", + "throat": "^4.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-junit": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-13.0.0.tgz", + "integrity": "sha512-JSHR+Dhb32FGJaiKkqsB7AR3OqWKtldLd6ZH2+FJ8D4tsweb8Id8zEVReU4+OlrRO1ZluqJLQEETm+Q6/KilBg==", + "dev": true, + "dependencies": { + "mkdirp": "^1.0.4", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2", + "xml": "^1.0.1" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/jest-junit/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-leak-detector": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-24.9.0.tgz", + "integrity": "sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA==", + "dev": true, + "dependencies": { + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-matcher-utils": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz", + "integrity": "sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==", + "dev": true, + "dependencies": { + "chalk": "^2.0.1", + "jest-diff": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-message-util": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-24.9.0.tgz", + "integrity": "sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/stack-utils": "^1.0.1", + "chalk": "^2.0.1", + "micromatch": "^3.1.10", + "slash": "^2.0.0", + "stack-utils": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-mock": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-24.9.0.tgz", + "integrity": "sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz", + "integrity": "sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-resolve": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-24.9.0.tgz", + "integrity": "sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "browser-resolve": "^1.11.3", + "chalk": "^2.0.1", + "jest-pnp-resolver": "^1.2.1", + "realpath-native": "^1.1.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-24.9.0.tgz", + "integrity": "sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-snapshot": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-runner": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-24.9.0.tgz", + "integrity": "sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg==", + "dev": true, + "dependencies": { + "@jest/console": "^24.7.1", + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "chalk": "^2.4.2", + "exit": "^0.1.2", + "graceful-fs": "^4.1.15", + "jest-config": "^24.9.0", + "jest-docblock": "^24.3.0", + "jest-haste-map": "^24.9.0", + "jest-jasmine2": "^24.9.0", + "jest-leak-detector": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-resolve": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-util": "^24.9.0", + "jest-worker": "^24.6.0", + "source-map-support": "^0.5.6", + "throat": "^4.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-runtime": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-24.9.0.tgz", + "integrity": "sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw==", + "dev": true, + "dependencies": { + "@jest/console": "^24.7.1", + "@jest/environment": "^24.9.0", + "@jest/source-map": "^24.3.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/yargs": "^13.0.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.1.15", + "jest-config": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-regex-util": "^24.3.0", + "jest-resolve": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "realpath-native": "^1.1.0", + "slash": "^2.0.0", + "strip-bom": "^3.0.0", + "yargs": "^13.3.0" + }, + "bin": { + "jest-runtime": "bin/jest-runtime.js" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-runtime/node_modules/cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/jest-runtime/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "node_modules/jest-runtime/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-runtime/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-runtime/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-runtime/node_modules/wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-runtime/node_modules/yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "dependencies": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "node_modules/jest-runtime/node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/jest-serializer": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-24.9.0.tgz", + "integrity": "sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-serializer-vue": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jest-serializer-vue/-/jest-serializer-vue-2.0.2.tgz", + "integrity": "sha512-nK/YIFo6qe3i9Ge+hr3h4PpRehuPPGZFt8LDBdTHYldMb7ZWlkanZS8Ls7D8h6qmQP2lBQVDLP0DKn5bJ9QApQ==", + "dev": true, + "dependencies": { + "pretty": "2.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-24.9.0.tgz", + "integrity": "sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0", + "@jest/types": "^24.9.0", + "chalk": "^2.0.1", + "expect": "^24.9.0", + "jest-diff": "^24.9.0", + "jest-get-type": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-resolve": "^24.9.0", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^24.9.0", + "semver": "^6.2.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-transform-stub": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jest-transform-stub/-/jest-transform-stub-2.0.0.tgz", + "integrity": "sha512-lspHaCRx/mBbnm3h4uMMS3R5aZzMwyNpNIJLXj4cEsV0mIUtS4IjYJLSoyjRCtnxb6RIGJ4NL2quZzfIeNhbkg==", + "dev": true + }, + "node_modules/jest-util": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-24.9.0.tgz", + "integrity": "sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg==", + "dev": true, + "dependencies": { + "@jest/console": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/source-map": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "callsites": "^3.0.0", + "chalk": "^2.0.1", + "graceful-fs": "^4.1.15", + "is-ci": "^2.0.0", + "mkdirp": "^0.5.1", + "slash": "^2.0.0", + "source-map": "^0.6.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-util/node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-validate": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-24.9.0.tgz", + "integrity": "sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "camelcase": "^5.3.1", + "chalk": "^2.0.1", + "jest-get-type": "^24.9.0", + "leven": "^3.1.0", + "pretty-format": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-watch-typeahead": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-0.4.2.tgz", + "integrity": "sha512-f7VpLebTdaXs81rg/oj4Vg/ObZy2QtGzAmGLNsqUS5G5KtSN68tFcIsbvNODfNyQxU78g7D8x77o3bgfBTR+2Q==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^2.4.1", + "jest-regex-util": "^24.9.0", + "jest-watcher": "^24.3.0", + "slash": "^3.0.0", + "string-length": "^3.1.0", + "strip-ansi": "^5.0.0" + } + }, + "node_modules/jest-watch-typeahead/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead/node_modules/string-length": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz", + "integrity": "sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA==", + "dev": true, + "dependencies": { + "astral-regex": "^1.0.0", + "strip-ansi": "^5.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watch-typeahead/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-watcher": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-24.9.0.tgz", + "integrity": "sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/yargs": "^13.0.0", + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.1", + "jest-util": "^24.9.0", + "string-length": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-watcher/node_modules/ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/jest-worker": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz", + "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==", + "dev": true, + "dependencies": { + "merge-stream": "^2.0.0", + "supports-color": "^6.1.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/js-beautify": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", + "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==", + "dev": true, + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.3.3", + "js-cookie": "^3.0.5", + "nopt": "^7.2.0" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-beautify/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/js-beautify/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-beautify/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-message": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz", + "integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==", + "dev": true, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, + "node_modules/jsdom": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", + "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", + "dev": true, + "dependencies": { + "abab": "^2.0.0", + "acorn": "^5.5.3", + "acorn-globals": "^4.1.0", + "array-equal": "^1.0.0", + "cssom": ">= 0.3.2 < 0.4.0", + "cssstyle": "^1.0.0", + "data-urls": "^1.0.0", + "domexception": "^1.0.1", + "escodegen": "^1.9.1", + "html-encoding-sniffer": "^1.0.2", + "left-pad": "^1.3.0", + "nwsapi": "^2.0.7", + "parse5": "4.0.0", + "pn": "^1.1.0", + "request": "^2.87.0", + "request-promise-native": "^1.0.5", + "sax": "^1.2.4", + "symbol-tree": "^3.2.2", + "tough-cookie": "^2.3.4", + "w3c-hr-time": "^1.0.1", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.3", + "whatwg-mimetype": "^2.1.0", + "whatwg-url": "^6.4.1", + "ws": "^5.2.0", + "xml-name-validator": "^3.0.0" + } + }, + "node_modules/jsdom/node_modules/acorn": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/jsdom/node_modules/parse5": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", + "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", + "dev": true + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dev": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/killable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", + "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", + "dev": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/known-css-properties": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.21.0.tgz", + "integrity": "sha512-sZLUnTqimCkvkgRS+kbPlYW5o8q5w1cu+uIisKpEWkj31I8mx8kNG162DwRav8Zirkva6N5uoFsm9kzK4mUXjw==", + "dev": true + }, + "node_modules/launch-editor": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", + "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/launch-editor-middleware": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor-middleware/-/launch-editor-middleware-2.9.1.tgz", + "integrity": "sha512-4wF6AtPtaIENiZdH/a+3yW8Xni7uxzTEDd1z+gH00hUWBCSmQknFohznMd9BWhLk8MXObeB5ir69GbIr9qFW1w==", + "dev": true, + "dependencies": { + "launch-editor": "^2.9.1" + } + }, + "node_modules/left-pad": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", + "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==", + "deprecated": "use String.prototype.padStart()", + "dev": true + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/loader-fs-cache": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/loader-fs-cache/-/loader-fs-cache-1.0.3.tgz", + "integrity": "sha512-ldcgZpjNJj71n+2Mf6yetz+c9bM4xpKtNds4LbqXzU/PTdeAX0g3ytnU1AJMEcTk2Lex4Smpe3Q/eCTsvUBxbA==", + "dev": true, + "dependencies": { + "find-cache-dir": "^0.1.1", + "mkdirp": "^0.5.1" + } + }, + "node_modules/loader-fs-cache/node_modules/find-cache-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-0.1.1.tgz", + "integrity": "sha512-Z9XSBoNE7xQiV6MSgPuCfyMokH2K7JdpRkOYE1+mu3d4BFJtx3GW+f6Bo4q8IX6rlf5MYbLBKW0pjl2cWdkm2A==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "mkdirp": "^0.5.1", + "pkg-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-fs-cache/node_modules/find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", + "dev": true, + "dependencies": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-fs-cache/node_modules/path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", + "dev": true, + "dependencies": { + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-fs-cache/node_modules/pkg-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", + "integrity": "sha512-c6pv3OE78mcZ92ckebVDqg0aWSoKhOTbwCV6qbCWMk546mAL9pZln0+QsN/yQ7fkucd4+yJPLrCBXNt8Ruk+Eg==", + "dev": true, + "dependencies": { + "find-up": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", + "dev": true, + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.defaultsdeep": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz", + "integrity": "sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==", + "dev": true + }, + "node_modules/lodash.find": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.find/-/lodash.find-4.6.0.tgz", + "integrity": "sha512-yaRZoAV3Xq28F1iafWN1+a0rflOej93l1DQUejs3SZ41h2O9UJBoS9aueGjPDgAl4B6tPC0NuuchLKaDQQ3Isg==", + "dev": true + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "dev": true + }, + "node_modules/lodash.mapvalues": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==", + "dev": true + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "node_modules/lodash.transform": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz", + "integrity": "sha512-LO37ZnhmBVx0GvOU/caQuipEh4GN82TcWv3yHlebGDgOxbxiwwzW5Pcx2AcvpIv2WmvmSMoC492yQFNhy/l/UQ==", + "dev": true + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "dependencies": { + "chalk": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/longest-streak": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz", + "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "dev": true, + "dependencies": { + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mathml-tag-names": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", + "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", + "integrity": "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==", + "dev": true, + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-string": "^2.0.0", + "micromark": "~2.11.0", + "parse-entities": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz", + "integrity": "sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "longest-streak": "^2.0.0", + "mdast-util-to-string": "^2.0.0", + "parse-entities": "^2.0.0", + "repeat-string": "^1.0.0", + "zwitch": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", + "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", + "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", + "dev": true + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-fs": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz", + "integrity": "sha512-+y4mDxU4rvXXu5UDSGCGNiesFmwCHuefGMoPCO1WYucNYj7DsLqrFaa2fXVI0H+NNiPTwwzKwspn9yTZqUGqng==", + "dev": true + }, + "node_modules/meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dev": true, + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/meow/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/meow/node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/meow/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-source-map": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", + "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", + "dev": true, + "dependencies": { + "source-map": "^0.6.1" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromark": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", + "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "debug": "^4.0.0", + "parse-entities": "^2.0.0" + } + }, + "node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz", + "integrity": "sha512-lp3GeY7ygcgAmVIcRPBVhIkf8Us7FZjA+ILpal44qLdSu11wmjKQ3d9k15lfD7pO4esu9eUIAW7qiYIBppv40A==", + "dev": true, + "dependencies": { + "loader-utils": "^1.1.0", + "normalize-url": "1.9.1", + "schema-utils": "^1.0.0", + "webpack-sources": "^1.1.0" + }, + "engines": { + "node": ">= 6.9.0" + }, + "peerDependencies": { + "webpack": "^4.4.0" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minimist-options/node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "dev": true, + "dependencies": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "dev": true, + "dependencies": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==", + "dev": true + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "dev": true, + "optional": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node_modules/no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "dev": true, + "dependencies": { + "lower-case": "^1.1.1" + } + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true + }, + "node_modules/node-cache": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-4.2.1.tgz", + "integrity": "sha512-BOb67bWg2dTyax5kdef5WfU3X8xu4wPg+zHzkvls0Q/QpYycIFRLEEIdAx9Wma43DxG6Qzn4illdZoYseKWa4A==", + "dev": true, + "dependencies": { + "clone": "2.x", + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 0.4.6" + } + }, + "node_modules/node-forge": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", + "dev": true, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "dependencies": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + } + }, + "node_modules/node-libs-browser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true + }, + "node_modules/node-notifier": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.5.tgz", + "integrity": "sha512-tVbHs7DyTLtzOiN78izLA85zRqB9NvEXkAf014Vx3jtSvn/xBl6bR8ZYifj+dFcFrKI21huSQgJZ6ZtL3B4HfQ==", + "dev": true, + "dependencies": { + "growly": "^1.3.0", + "is-wsl": "^1.1.0", + "semver": "^5.5.0", + "shellwords": "^0.1.1", + "which": "^1.3.0" + } + }, + "node_modules/node-notifier/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/node-notifier/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-selector": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/normalize-selector/-/normalize-selector-0.2.0.tgz", + "integrity": "sha512-dxvWdI8gw6eAvk9BlPffgEoGfM7AdijoCwOEJge3e3ulT2XLgmU7KvvxprOaCu05Q1uGRHmOhHe1r6emZoKyFw==", + "dev": true + }, + "node_modules/normalize-url": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.9.1.tgz", + "integrity": "sha512-A48My/mtCklowHBlI8Fq2jFWK4tX4lJ5E6ytFsSOq1fzpvT0SQSgKhSg7lN5c2uYFOrUAOQp6zhhJnpp1eMloQ==", + "dev": true, + "dependencies": { + "object-assign": "^4.0.1", + "prepend-http": "^1.0.0", + "query-string": "^4.1.0", + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/null-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/null-loader/-/null-loader-4.0.1.tgz", + "integrity": "sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/null-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==", + "dev": true + }, + "node_modules/nwsapi": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", + "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==", + "dev": true + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", + "dev": true, + "dependencies": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/object-copy/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-copy/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.3.1.tgz", + "integrity": "sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "dev": true, + "dependencies": { + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.getownpropertydescriptors": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.8.tgz", + "integrity": "sha512-qkHIGe4q0lSYMv0XI4SsBTJz3WaURhLvd0lKSgtVuOsJ2krg4SgMw3PIRQFMp07yi++UR3se2mkcLqsBNpBb/A==", + "dev": true, + "dependencies": { + "array.prototype.reduce": "^1.0.6", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "gopd": "^1.0.1", + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-6.4.0.tgz", + "integrity": "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==", + "dev": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/open/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/opn": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", + "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", + "dev": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/opn/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", + "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", + "dev": true, + "dependencies": { + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-spinners": "^2.0.0", + "log-symbols": "^2.2.0", + "strip-ansi": "^5.2.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ora/node_modules/cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "dev": true, + "dependencies": { + "restore-cursor": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ora/node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ora/node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ora/node_modules/restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "dev": true, + "dependencies": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", + "dev": true + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-each-series": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-1.0.0.tgz", + "integrity": "sha512-J/e9xiZZQNrt+958FFzJ+auItsBGq+UrQ7nE89AUP7UOTtjHnkISANXLdayhVzh538UnLMCSlf13lFfRIAKQOA==", + "dev": true, + "dependencies": { + "p-reduce": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-reduce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", + "integrity": "sha512-3Tx1T3oM1xO/Y8Gj0sWyE78EIJZ+t+aEmXUdvQgvGmSMri7aPTHoovbXEreWKkL5j21Er60XAWLTzKbAKYOujQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-retry": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", + "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", + "dev": true, + "dependencies": { + "retry": "^0.12.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "node_modules/parallel-transform": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", + "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "dev": true, + "dependencies": { + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "node_modules/param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==", + "dev": true, + "dependencies": { + "no-case": "^2.2.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module/node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-asn1": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", + "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", + "dev": true, + "dependencies": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "hash-base": "~3.0", + "pbkdf2": "^3.1.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-asn1/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dev": true, + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "dev": true + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "node_modules/path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", + "dev": true + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true + }, + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "dev": true + }, + "node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-type/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pbkdf2": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "dev": true, + "dependencies": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "dev": true + }, + "node_modules/pnp-webpack-plugin": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.7.0.tgz", + "integrity": "sha512-2Rb3vm+EXble/sMXNSu6eoBx8e79gKqhNq9F5ZWW6ERNCTE/Q0wQNne5541tE5vKjfM8hpNCYL+LGc1YTfI0dg==", + "dev": true, + "dependencies": { + "ts-pnp": "^1.1.6" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/portfinder": { + "version": "1.0.32", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", + "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", + "dev": true, + "dependencies": { + "async": "^2.6.4", + "debug": "^3.2.7", + "mkdirp": "^0.5.6" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss-calc": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.5.tgz", + "integrity": "sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==", + "dev": true, + "dependencies": { + "postcss": "^7.0.27", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/postcss-colormin": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", + "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "color": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-colormin/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-convert-values": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", + "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-convert-values/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-discard-comments": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", + "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", + "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-discard-empty": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", + "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", + "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-html": { + "version": "0.36.0", + "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-0.36.0.tgz", + "integrity": "sha512-HeiOxGcuwID0AFsNAL0ox3mW6MHH5cstWN1Z3Y+n6H+g12ih7LHdYxWwEA/QmrebctLjo79xz9ouK3MroHwOJw==", + "dev": true, + "dependencies": { + "htmlparser2": "^3.10.0" + }, + "peerDependencies": { + "postcss": ">=5.0.0", + "postcss-syntax": ">=0.36.0" + } + }, + "node_modules/postcss-html/node_modules/dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, + "node_modules/postcss-html/node_modules/dom-serializer/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/postcss-html/node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/postcss-html/node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "node_modules/postcss-html/node_modules/domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "dependencies": { + "domelementtype": "1" + } + }, + "node_modules/postcss-html/node_modules/domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/postcss-html/node_modules/entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true + }, + "node_modules/postcss-html/node_modules/htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "dependencies": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "node_modules/postcss-html/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-less": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-3.1.4.tgz", + "integrity": "sha512-7TvleQWNM2QLcHqvudt3VYjULVB49uiW6XzEUFmvwHzvsOEF5MwBrIXZDJQvJNFGjJQTzSzZnDoCJ8h/ljyGXA==", + "dev": true, + "dependencies": { + "postcss": "^7.0.14" + }, + "engines": { + "node": ">=6.14.4" + } + }, + "node_modules/postcss-load-config": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.2.tgz", + "integrity": "sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw==", + "dev": true, + "dependencies": { + "cosmiconfig": "^5.0.0", + "import-cwd": "^2.0.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss-loader": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-3.0.0.tgz", + "integrity": "sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==", + "dev": true, + "dependencies": { + "loader-utils": "^1.1.0", + "postcss": "^7.0.0", + "postcss-load-config": "^2.0.0", + "schema-utils": "^1.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/postcss-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/postcss-loader/node_modules/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true + }, + "node_modules/postcss-merge-longhand": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", + "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", + "dev": true, + "dependencies": { + "css-color-names": "0.0.4", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "stylehacks": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-merge-longhand/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-merge-rules": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", + "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "cssnano-util-same-parent": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0", + "vendors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-merge-rules/node_modules/postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "dev": true, + "dependencies": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", + "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-minify-font-values/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-minify-gradients": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", + "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", + "dev": true, + "dependencies": { + "cssnano-util-get-arguments": "^4.0.0", + "is-color-stop": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-minify-gradients/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-minify-params": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", + "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", + "dev": true, + "dependencies": { + "alphanum-sort": "^1.0.0", + "browserslist": "^4.0.0", + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "uniqs": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-minify-params/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-minify-selectors": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", + "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", + "dev": true, + "dependencies": { + "alphanum-sort": "^1.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-minify-selectors/node_modules/postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "dev": true, + "dependencies": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", + "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", + "dev": true, + "dependencies": { + "postcss": "^7.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz", + "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==", + "dev": true, + "dependencies": { + "icss-utils": "^4.1.1", + "postcss": "^7.0.32", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-modules-scope": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz", + "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==", + "dev": true, + "dependencies": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^6.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-modules-values": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", + "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", + "dev": true, + "dependencies": { + "icss-utils": "^4.0.0", + "postcss": "^7.0.6" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", + "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", + "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", + "dev": true, + "dependencies": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-normalize-display-values/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-normalize-positions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", + "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", + "dev": true, + "dependencies": { + "cssnano-util-get-arguments": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-normalize-positions/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", + "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", + "dev": true, + "dependencies": { + "cssnano-util-get-arguments": "^4.0.0", + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-normalize-repeat-style/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-normalize-string": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", + "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", + "dev": true, + "dependencies": { + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-normalize-string/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", + "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", + "dev": true, + "dependencies": { + "cssnano-util-get-match": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-normalize-timing-functions/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-normalize-unicode": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", + "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-normalize-unicode/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-normalize-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", + "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", + "dev": true, + "dependencies": { + "is-absolute-url": "^2.0.0", + "normalize-url": "^3.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-normalize-url/node_modules/normalize-url": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", + "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/postcss-normalize-url/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-normalize-whitespace": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", + "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-normalize-whitespace/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-ordered-values": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", + "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", + "dev": true, + "dependencies": { + "cssnano-util-get-arguments": "^4.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-ordered-values/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-reduce-initial": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", + "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-api": "^3.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", + "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", + "dev": true, + "dependencies": { + "cssnano-util-get-match": "^4.0.0", + "has": "^1.0.0", + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-reduce-transforms/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-resolve-nested-selector": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz", + "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==", + "dev": true + }, + "node_modules/postcss-safe-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz", + "integrity": "sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g==", + "dev": true, + "dependencies": { + "postcss": "^7.0.26" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-sass": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/postcss-sass/-/postcss-sass-0.4.4.tgz", + "integrity": "sha512-BYxnVYx4mQooOhr+zer0qWbSPYnarAy8ZT7hAQtbxtgVf8gy+LSLT/hHGe35h14/pZDTw1DsxdbrwxBN++H+fg==", + "dev": true, + "dependencies": { + "gonzales-pe": "^4.3.0", + "postcss": "^7.0.21" + } + }, + "node_modules/postcss-scss": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-2.1.1.tgz", + "integrity": "sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA==", + "dev": true, + "dependencies": { + "postcss": "^7.0.6" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-sorting": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-5.0.1.tgz", + "integrity": "sha512-Y9fUFkIhfrm6i0Ta3n+89j56EFqaNRdUKqXyRp6kvTcSXnmgEjaVowCXH+JBe9+YKWqd4nc28r2sgwnzJalccA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14", + "postcss": "^7.0.17" + }, + "engines": { + "node": ">=8.7.0" + } + }, + "node_modules/postcss-svgo": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.3.tgz", + "integrity": "sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw==", + "dev": true, + "dependencies": { + "postcss": "^7.0.0", + "postcss-value-parser": "^3.0.0", + "svgo": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-svgo/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "dev": true + }, + "node_modules/postcss-syntax": { + "version": "0.36.2", + "resolved": "https://registry.npmjs.org/postcss-syntax/-/postcss-syntax-0.36.2.tgz", + "integrity": "sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==", + "dev": true, + "peerDependencies": { + "postcss": ">=5.0.0" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", + "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", + "dev": true, + "dependencies": { + "alphanum-sort": "^1.0.0", + "postcss": "^7.0.0", + "uniqs": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/postcss/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prettier": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", + "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", + "devOptional": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pretty/-/pretty-2.0.0.tgz", + "integrity": "sha512-G9xUchgTEiNpormdYBl+Pha50gOUovT18IvAe7EYMZ1/f9W/WWMPRn+xI68yXNMUk3QXHDwo/1wV/4NejVNe1w==", + "dev": true, + "dependencies": { + "condense-newlines": "^0.2.1", + "extend-shallow": "^2.0.1", + "js-beautify": "^1.6.12" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pretty-error": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", + "integrity": "sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^2.0.4" + } + }, + "node_modules/pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pretty/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pretty/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "dependencies": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "node_modules/pumpify/node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/query-string": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", + "integrity": "sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/read-pkg/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/realpath-native": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz", + "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", + "dev": true, + "dependencies": { + "util.promisify": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", + "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true, + "engines": { + "node": ">=6.5.0" + } + }, + "node_modules/regexpu-core": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.1.1.tgz", + "integrity": "sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.11.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.1.tgz", + "integrity": "sha512-1DHODs4B8p/mQHU9kr+jv8+wIC9mtG4eBHxWxIq5mhjE3D5oORhCc6deRKzTjs9DcfRFmj9BHSDguZklqCGFWQ==", + "dev": true, + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remark": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/remark/-/remark-13.0.0.tgz", + "integrity": "sha512-HDz1+IKGtOyWN+QgBiAT0kn+2s6ovOxHyPAFGKVE81VSzJ+mq7RwHFledEvB5F1p4iJvOah/LOKdFuzvRnNLCA==", + "dev": true, + "dependencies": { + "remark-parse": "^9.0.0", + "remark-stringify": "^9.0.0", + "unified": "^9.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", + "integrity": "sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==", + "dev": true, + "dependencies": { + "mdast-util-from-markdown": "^0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-9.0.1.tgz", + "integrity": "sha512-mWmNg3ZtESvZS8fv5PTvaPckdL4iNlCHTt8/e/8oN08nArHRHjNZMKzA/YW3+p7/lYqIw4nx1XsjCBo/AxNChg==", + "dev": true, + "dependencies": { + "mdast-util-to-markdown": "^0.6.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true + }, + "node_modules/renderkid": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz", + "integrity": "sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==", + "dev": true, + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^3.0.1" + } + }, + "node_modules/renderkid/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/renderkid/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.19" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/request-promise-native": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", + "deprecated": "request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142", + "dev": true, + "dependencies": { + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "engines": { + "node": ">=0.12.0" + }, + "peerDependencies": { + "request": "^2.34" + } + }, + "node_modules/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "node_modules/require-package-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz", + "integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==", + "dev": true + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha512-ccu8zQTrzVr954472aUVPLEcB3YpKSYR3cg/3lo1okzobPBM+1INXBbBZlDbnI/hbEocnf8j0QVo43hQKrbchg==", + "dev": true, + "dependencies": { + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "deprecated": "https://github.com/lydell/resolve-url#deprecated", + "dev": true + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rgb-regex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", + "integrity": "sha512-gDK5mkALDFER2YLqH6imYvK6g02gpNGM4ILDZ472EwWfXZnC2ZEpoB2ECXTyOVUKuk/bPJZMzwQPBYICzP+D3w==", + "dev": true + }, + "node_modules/rgba-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "integrity": "sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg==", + "dev": true + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true, + "engines": { + "node": "6.* || >= 7.*" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg==", + "dev": true, + "dependencies": { + "aproba": "^1.1.1" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "dev": true, + "dependencies": { + "ret": "~0.1.10" + } + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sane": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", + "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "deprecated": "some dependency vulnerabilities fixed, support for node < 10 dropped, and newer ECMAScript syntax/features added", + "dev": true, + "dependencies": { + "@cnakazawa/watch": "^1.0.3", + "anymatch": "^2.0.0", + "capture-exit": "^2.0.0", + "exec-sh": "^0.3.2", + "execa": "^1.0.0", + "fb-watchman": "^2.0.0", + "micromatch": "^3.1.4", + "minimist": "^1.1.1", + "walker": "~1.0.5" + }, + "bin": { + "sane": "src/cli.js" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/sane/node_modules/anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "dependencies": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "node_modules/sane/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sass": { + "version": "1.32.13", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.32.13.tgz", + "integrity": "sha512-dEgI9nShraqP7cXQH+lEXVf73WOPCse0QlFzSD8k+1TcOxCMwVXfQlr0jtoluZysQOyJGnfr21dLvYKDJq8HkA==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/sass-loader": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.2.1.tgz", + "integrity": "sha512-RRvWl+3K2LSMezIsd008ErK4rk6CulIMSwrcc2aZvjymUgKo/vjXGp1rSWmfTUX7bblEOz8tst4wBwWtCGBqKA==", + "dev": true, + "dependencies": { + "klona": "^2.0.4", + "loader-utils": "^2.0.0", + "neo-async": "^2.6.2", + "schema-utils": "^3.0.0", + "semver": "^7.3.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0", + "sass": "^1.3.0", + "webpack": "^4.36.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/sass-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/sass-loader/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true + }, + "node_modules/saxes": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", + "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", + "dev": true, + "dependencies": { + "xmlchars": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "node_modules/selfsigned": { + "version": "1.10.14", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.14.tgz", + "integrity": "sha512-lkjaiAye+wBZDCBsu5BGi0XiLRxeUlsGod5ZP924CRSEoGuZAw/f7y9RKu28rwTfiHVhdavhB0qH0INV6P1lEA==", + "dev": true, + "dependencies": { + "node-forge": "^0.10.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "dependencies": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "dependencies": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-node/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "dependencies": { + "kind-of": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon-util/node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/snapdragon-util/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/snapdragon/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/snapdragon/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/sockjs-client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.6.1.tgz", + "integrity": "sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "eventsource": "^2.0.2", + "faye-websocket": "^0.11.4", + "inherits": "^2.0.4", + "url-parse": "^1.5.10" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://tidelift.com/funding/github/npm/sockjs-client" + } + }, + "node_modules/sockjs-client/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys/node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated", + "dev": true + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "dev": true + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/spdy-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/specificity": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/specificity/-/specificity-0.4.1.tgz", + "integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==", + "dev": true, + "bin": { + "specificity": "bin/specificity" + } + }, + "node_modules/split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dev": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", + "dev": true + }, + "node_modules/stack-utils": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.5.tgz", + "integrity": "sha512-KZiTzuV3CnSnSvgMRrARVCj+Ht7rMbauGDK0LdVFRGyenwdylpajAp4Q0i6SX8rEmbTpMMf6ryq2gb8pPq2WgQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "dev": true + }, + "node_modules/static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "dev": true, + "dependencies": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-extend/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "dependencies": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "node_modules/stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "dev": true + }, + "node_modules/strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-length": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", + "integrity": "sha512-Qka42GGrS8Mm3SZ+7cH8UXiIWI867/b/Z/feQSpQx/rbfB8UGknGEZVaUQMOUVj+soY6NpWAxily63HI1OckVQ==", + "dev": true, + "dependencies": { + "astral-regex": "^1.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-search": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", + "integrity": "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==", + "dev": true + }, + "node_modules/stylehacks": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", + "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "postcss": "^7.0.0", + "postcss-selector-parser": "^3.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/stylehacks/node_modules/postcss-selector-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", + "dev": true, + "dependencies": { + "dot-prop": "^5.2.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint": { + "version": "13.13.1", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-13.13.1.tgz", + "integrity": "sha512-Mv+BQr5XTUrKqAXmpqm6Ddli6Ief+AiPZkRsIrAoUKFuq/ElkUh9ZMYxXD0iQNZ5ADghZKLOWz1h7hTClB7zgQ==", + "dev": true, + "dependencies": { + "@stylelint/postcss-css-in-js": "^0.37.2", + "@stylelint/postcss-markdown": "^0.36.2", + "autoprefixer": "^9.8.6", + "balanced-match": "^2.0.0", + "chalk": "^4.1.1", + "cosmiconfig": "^7.0.0", + "debug": "^4.3.1", + "execall": "^2.0.0", + "fast-glob": "^3.2.5", + "fastest-levenshtein": "^1.0.12", + "file-entry-cache": "^6.0.1", + "get-stdin": "^8.0.0", + "global-modules": "^2.0.0", + "globby": "^11.0.3", + "globjoin": "^0.1.4", + "html-tags": "^3.1.0", + "ignore": "^5.1.8", + "import-lazy": "^4.0.0", + "imurmurhash": "^0.1.4", + "known-css-properties": "^0.21.0", + "lodash": "^4.17.21", + "log-symbols": "^4.1.0", + "mathml-tag-names": "^2.1.3", + "meow": "^9.0.0", + "micromatch": "^4.0.4", + "normalize-selector": "^0.2.0", + "postcss": "^7.0.35", + "postcss-html": "^0.36.0", + "postcss-less": "^3.1.4", + "postcss-media-query-parser": "^0.2.3", + "postcss-resolve-nested-selector": "^0.1.1", + "postcss-safe-parser": "^4.0.2", + "postcss-sass": "^0.4.4", + "postcss-scss": "^2.1.1", + "postcss-selector-parser": "^6.0.5", + "postcss-syntax": "^0.36.2", + "postcss-value-parser": "^4.1.0", + "resolve-from": "^5.0.0", + "slash": "^3.0.0", + "specificity": "^0.4.1", + "string-width": "^4.2.2", + "strip-ansi": "^6.0.0", + "style-search": "^0.1.0", + "sugarss": "^2.0.0", + "svg-tags": "^1.0.0", + "table": "^6.6.0", + "v8-compile-cache": "^2.3.0", + "write-file-atomic": "^3.0.3" + }, + "bin": { + "stylelint": "bin/stylelint.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + } + }, + "node_modules/stylelint-config-prettier": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/stylelint-config-prettier/-/stylelint-config-prettier-8.0.2.tgz", + "integrity": "sha512-TN1l93iVTXpF9NJstlvP7nOu9zY2k+mN0NSFQ/VEGz15ZIP9ohdDZTtCWHs5LjctAhSAzaILULGbgiM0ItId3A==", + "dev": true, + "bin": { + "stylelint-config-prettier": "bin/check.js", + "stylelint-config-prettier-check": "bin/check.js" + }, + "engines": { + "node": ">= 10", + "npm": ">= 5" + }, + "peerDependencies": { + "stylelint": ">=11.0.0" + } + }, + "node_modules/stylelint-config-recommended": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-5.0.0.tgz", + "integrity": "sha512-c8aubuARSu5A3vEHLBeOSJt1udOdS+1iue7BmJDTSXoCBmfEQmmWX+59vYIj3NQdJBY6a/QRv1ozVFpaB9jaqA==", + "dev": true, + "peerDependencies": { + "stylelint": "^13.13.0" + } + }, + "node_modules/stylelint-config-sass-guidelines": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-sass-guidelines/-/stylelint-config-sass-guidelines-8.0.0.tgz", + "integrity": "sha512-v21iDWtzpfhuKJlYKpoE1vjp+GT8Cr6ZBWwMx/jf+eCEblZgAIDVVjgAELoDLhVj17DcEFwlIKJBMfrdAmXg0Q==", + "dev": true, + "dependencies": { + "stylelint-order": "^4.0.0", + "stylelint-scss": "^3.18.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "stylelint": "^13.7.0" + } + }, + "node_modules/stylelint-config-suitcss": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-suitcss/-/stylelint-config-suitcss-17.0.0.tgz", + "integrity": "sha512-hBd6inIaU7pwmNPxfcPfeKwt3oje3dn7Oc23ghHDTMzRgaTFWJsMaNQGDf4cNYsV4K9juQUo74aMiSdCRiPAUw==", + "dev": true, + "dependencies": { + "stylelint-order": "^4.1.0", + "stylelint-suitcss": "^4.0.0" + }, + "peerDependencies": { + "stylelint": "^13.13.1" + } + }, + "node_modules/stylelint-order": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/stylelint-order/-/stylelint-order-4.1.0.tgz", + "integrity": "sha512-sVTikaDvMqg2aJjh4r48jsdfmqLT+nqB1MOsaBnvM3OwLx4S+WXcsxsgk5w18h/OZoxZCxuyXMh61iBHcj9Qiw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15", + "postcss": "^7.0.31", + "postcss-sorting": "^5.0.1" + }, + "peerDependencies": { + "stylelint": "^10.0.1 || ^11.0.0 || ^12.0.0 || ^13.0.0" + } + }, + "node_modules/stylelint-prettier": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/stylelint-prettier/-/stylelint-prettier-1.2.0.tgz", + "integrity": "sha512-/MYz6W2CNgKHblPzPtk7cybu8H5dGG3c2GevL64RButERj1uJg4SdBIIat1hMfDOmN6QQpldc6tCc//ZAWh9WQ==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "prettier": ">= 0.11.0", + "stylelint": ">= 9.2.1" + } + }, + "node_modules/stylelint-scss": { + "version": "3.20.1", + "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.20.1.tgz", + "integrity": "sha512-OTd55O1TTAC5nGKkVmUDLpz53LlK39R3MImv1CfuvsK7/qugktqiZAeQLuuC4UBhzxCnsc7fp9u/gfRZwFAIkA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15", + "postcss-media-query-parser": "^0.2.3", + "postcss-resolve-nested-selector": "^0.1.1", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "stylelint": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0" + } + }, + "node_modules/stylelint-suitcss": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/stylelint-suitcss/-/stylelint-suitcss-4.0.0.tgz", + "integrity": "sha512-oVTd6ga/CoAssjm0hiTLJu7vMa6ib/xCJz1eGAIJTs+eW1s4WhCyc5zNTk3mD5i2PmCeY/sM8QzXPGKz3tYkDw==", + "dev": true, + "dependencies": { + "lodash.find": "^4.6.0", + "postcss-selector-parser": "^6.0.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "stylelint": "^13.13.1" + } + }, + "node_modules/stylelint-webpack-plugin": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/stylelint-webpack-plugin/-/stylelint-webpack-plugin-2.2.2.tgz", + "integrity": "sha512-zfIsAF13xe6xuhwxZDFWQEmuVcxnRk9JFovyRL/23CWjPK1HLRw4QZdvo0Bz1bQZaDQA+6ha7cU0NO+LXaw4Mw==", + "dev": true, + "dependencies": { + "@types/stylelint": "^13.13.0", + "arrify": "^2.0.1", + "globby": "^11.0.4", + "jest-worker": "^27.0.2", + "micromatch": "^4.0.4", + "normalize-path": "^3.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "stylelint": "^13.0.0", + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/stylelint-webpack-plugin/node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/stylelint-webpack-plugin/node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint-webpack-plugin/node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint-webpack-plugin/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint-webpack-plugin/node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint-webpack-plugin/node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/stylelint-webpack-plugin/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint-webpack-plugin/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/stylelint-webpack-plugin/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylelint-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint-webpack-plugin/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/stylelint-webpack-plugin/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/stylelint-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/stylelint-webpack-plugin/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/stylelint-webpack-plugin/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/stylelint-webpack-plugin/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/stylelint-webpack-plugin/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/stylelint/node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/stylelint/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/stylelint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/stylelint/node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/balanced-match": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", + "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", + "dev": true + }, + "node_modules/stylelint/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/stylelint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/stylelint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/stylelint/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stylelint/node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/stylelint/node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/stylelint/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/stylelint/node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/stylelint/node_modules/get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylelint/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/stylelint/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylelint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/stylelint/node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylelint/node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/stylelint/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/stylelint/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/stylelint/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylelint/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/stylelint/node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylelint/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/stylelint/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/stylelint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/table": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.2.tgz", + "integrity": "sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/stylelint/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/stylelint/node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/sugarss": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-2.0.0.tgz", + "integrity": "sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ==", + "dev": true, + "dependencies": { + "postcss": "^7.0.2" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "node_modules/svgo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", + "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", + "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", + "dev": true, + "dependencies": { + "chalk": "^2.4.1", + "coa": "^2.0.2", + "css-select": "^2.0.0", + "css-select-base-adapter": "^0.1.1", + "css-tree": "1.0.0-alpha.37", + "csso": "^4.0.2", + "js-yaml": "^3.13.1", + "mkdirp": "~0.5.1", + "object.values": "^1.1.0", + "sax": "~1.2.4", + "stable": "^0.1.8", + "unquote": "~1.1.1", + "util.promisify": "~1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/svgo/node_modules/css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" + } + }, + "node_modules/svgo/node_modules/css-what": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", + "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/svgo/node_modules/dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, + "node_modules/svgo/node_modules/domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/svgo/node_modules/domutils/node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "node_modules/svgo/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/svgo/node_modules/nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "dependencies": { + "boolbase": "~1.0.0" + } + }, + "node_modules/svgo/node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "node_modules/table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "dependencies": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/table/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "node_modules/table/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/table/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tapable": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz", + "integrity": "sha512-jX8Et4hHg57mug1/079yitEKWGB3LCwoxByLsNim89LABq8NqgiX+6iYVOsq0vX8uJHkU+DZ5fnq95f800bEsQ==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/terser": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", + "dev": true, + "dependencies": { + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.6.tgz", + "integrity": "sha512-2lBVf/VMVIddjSn3GqbT90GvIJ/eYXJkt8cTzU7NbjKqK8fwv18Ftr4PlbF46b/e88743iZFL5Dtr/rC4hjIeA==", + "dev": true, + "dependencies": { + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^4.0.0", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" + }, + "engines": { + "node": ">= 6.9.0" + }, + "peerDependencies": { + "webpack": "^4.0.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser-webpack-plugin/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/terser-webpack-plugin/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser-webpack-plugin/node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/terser-webpack-plugin/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/test-exclude": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", + "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "dev": true, + "dependencies": { + "glob": "^7.1.3", + "minimatch": "^3.0.4", + "read-pkg-up": "^4.0.0", + "require-main-filename": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/test-exclude/node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/test-exclude/node_modules/read-pkg-up": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", + "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", + "dev": true, + "dependencies": { + "find-up": "^3.0.0", + "read-pkg": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/thread-loader": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/thread-loader/-/thread-loader-2.1.3.tgz", + "integrity": "sha512-wNrVKH2Lcf8ZrWxDF/khdlLlsTMczdcwPA9VEK4c2exlEPynYWxi9op3nPTo5lAnDIkE0rQEB3VBP+4Zncc9Hg==", + "dev": true, + "dependencies": { + "loader-runner": "^2.3.1", + "loader-utils": "^1.1.0", + "neo-async": "^2.6.0" + }, + "engines": { + "node": ">= 6.9.0 <7.0.0 || >= 8.9.0" + }, + "peerDependencies": { + "webpack": "^2.0.0 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/thread-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/thread-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/throat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", + "integrity": "sha512-wCVxLDcFxw7ujDxaeJC6nfl2XfHJNYs8yUYJnvMgtPEFlttP9tHSfRUv2vBe6C4hkVFPWoP1P6ZccbYjmSEkKA==", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "dev": true, + "dependencies": { + "setimmediate": "^1.0.4" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-object-path/node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/to-object-path/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "dependencies": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "dev": true, + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toposort": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz", + "integrity": "sha512-FclLrw8b9bMWf4QlCJuHBEVhSRsqDj6u3nIjAzPeJvgl//1hBlffdlk0MALceL14+koWEdU4ofRAXofbODxQzg==", + "dev": true + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/trough": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", + "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tryer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", + "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", + "dev": true + }, + "node_modules/ts-jest": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-24.3.0.tgz", + "integrity": "sha512-Hb94C/+QRIgjVZlJyiWwouYUF+siNJHJHknyspaOcZ+OQAIdFG/UrdQVXw/0B8Z3No34xkUXZJpOTy9alOWdVQ==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "buffer-from": "1.x", + "fast-json-stable-stringify": "2.x", + "json5": "2.x", + "lodash.memoize": "4.x", + "make-error": "1.x", + "mkdirp": "0.x", + "resolve": "1.x", + "semver": "^5.5", + "yargs-parser": "10.x" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "jest": ">=24 <25" + } + }, + "node_modules/ts-jest/node_modules/camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/ts-jest/node_modules/yargs-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", + "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "dev": true, + "dependencies": { + "camelcase": "^4.1.0" + } + }, + "node_modules/ts-pnp": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz", + "integrity": "sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==", + "dev": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dev": true, + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "dev": true + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typeface-roboto": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/typeface-roboto/-/typeface-roboto-1.1.13.tgz", + "integrity": "sha512-YXvbd3a1QTREoD+FJoEkl0VQNJoEjewR2H11IjVv4bp6ahuIcw0yyw/3udC4vJkHw3T3cUh85FTg8eWef3pSaw==" + }, + "node_modules/uglify-js": { + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz", + "integrity": "sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==", + "dev": true, + "dependencies": { + "commander": "~2.19.0", + "source-map": "~0.6.1" + }, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/uglify-js/node_modules/commander": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", + "dev": true + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unified": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", + "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", + "dev": true, + "dependencies": { + "bail": "^1.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^2.0.0", + "trough": "^1.0.0", + "vfile": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unified/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/union-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==", + "dev": true + }, + "node_modules/uniqs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", + "integrity": "sha512-mZdDpf3vBV5Efh29kMw5tXoup/buMgxLzOt/XKFKcVmi+15ManNQWr6HfZ2aiZTYlYixbdNJ0KFmIZIv52tHSQ==", + "dev": true + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/unist-util-find-all-after": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/unist-util-find-all-after/-/unist-util-find-all-after-3.0.2.tgz", + "integrity": "sha512-xaTC/AGZ0rIM2gM28YVRAFPIZpzbpDtU3dRmp7EXlNVA8ziQc4hY3H7BHXM1J49nEmiqc3svnqMReW+PGqbZKQ==", + "dev": true, + "dependencies": { + "unist-util-is": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", + "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", + "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unquote": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", + "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==", + "dev": true + }, + "node_modules/unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "dev": true, + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "dev": true, + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==", + "dev": true + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "deprecated": "Please see https://github.com/lydell/urix#deprecated", + "dev": true + }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "dev": true, + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/url-loader": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-2.3.0.tgz", + "integrity": "sha512-goSdg8VY+7nPZKUEChZSEtW5gjbS66USIGCeSJ1OVOJ7Yfuh/36YxCwMi5HVEJh6mqUYOoy3NJ0vlOMrWsSHog==", + "dev": true, + "dependencies": { + "loader-utils": "^1.2.3", + "mime": "^2.4.4", + "schema-utils": "^2.5.0" + }, + "engines": { + "node": ">= 8.9.0" + }, + "peerDependencies": { + "file-loader": "*", + "webpack": "^4.0.0" + }, + "peerDependenciesMeta": { + "file-loader": { + "optional": true + } + } + }, + "node_modules/url-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/url-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true + }, + "node_modules/url/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/util.promisify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", + "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" + } + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", + "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", + "dev": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vendors": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", + "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, + "node_modules/vfile": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", + "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^2.0.0", + "vfile-message": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", + "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", + "dev": true, + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true + }, + "node_modules/vue": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz", + "integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==", + "deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.", + "dependencies": { + "@vue/compiler-sfc": "2.7.16", + "csstype": "^3.1.0" + } + }, + "node_modules/vue-cli-plugin-vuetify": { + "version": "2.4.8", + "resolved": "https://registry.npmjs.org/vue-cli-plugin-vuetify/-/vue-cli-plugin-vuetify-2.4.8.tgz", + "integrity": "sha512-1e8tVbJNQPLpdJgx8tlsCrFVSKrohLe5axWwolOuMr9k++X1pg95jiqBxYZdhh7tIl9bNh4wzVPPGQzTIpoS+Q==", + "dev": true, + "dependencies": { + "null-loader": "^4.0.1", + "semver": "^7.1.2", + "shelljs": "^0.8.3" + }, + "peerDependenciesMeta": { + "sass-loader": { + "optional": true + }, + "vuetify-loader": { + "optional": true + } + } + }, + "node_modules/vue-cli-plugin-vuetify/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vue-eslint-parser": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-8.3.0.tgz", + "integrity": "sha512-dzHGG3+sYwSf6zFBa0Gi9ZDshD7+ad14DGOdTLjruRVgZXe2J+DcZ9iUhyR48z5g1PqRa20yt3Njna/veLJL/g==", + "dev": true, + "dependencies": { + "debug": "^4.3.2", + "eslint-scope": "^7.0.0", + "eslint-visitor-keys": "^3.1.0", + "espree": "^9.0.0", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/acorn": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vue-hot-reload-api": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", + "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==", + "dev": true + }, + "node_modules/vue-jest": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vue-jest/-/vue-jest-3.0.7.tgz", + "integrity": "sha512-PIOxFM+wsBMry26ZpfBvUQ/DGH2hvp5khDQ1n51g3bN0TwFwTy4J85XVfxTRMukqHji/GnAoGUnlZ5Ao73K62w==", + "dev": true, + "dependencies": { + "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", + "chalk": "^2.1.0", + "deasync": "^0.1.15", + "extract-from-css": "^0.4.4", + "find-babel-config": "^1.1.0", + "js-beautify": "^1.6.14", + "node-cache": "^4.1.1", + "object-assign": "^4.1.1", + "source-map": "^0.5.6", + "tsconfig": "^7.0.0", + "vue-template-es2015-compiler": "^1.6.0" + }, + "peerDependencies": { + "babel-core": "^6.25.0 || ^7.0.0-0", + "vue": "^2.x", + "vue-template-compiler": "^2.x" + } + }, + "node_modules/vue-jest/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vue-loader": { + "version": "15.11.1", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.11.1.tgz", + "integrity": "sha512-0iw4VchYLePqJfJu9s62ACWUXeSqM30SQqlIftbYWM3C+jpPcEHKSPUZBLjSF9au4HTHQ/naF6OGnO3Q/qGR3Q==", + "dev": true, + "dependencies": { + "@vue/component-compiler-utils": "^3.1.0", + "hash-sum": "^1.0.2", + "loader-utils": "^1.1.0", + "vue-hot-reload-api": "^2.3.0", + "vue-style-loader": "^4.1.0" + }, + "peerDependencies": { + "css-loader": "*", + "webpack": "^3.0.0 || ^4.1.0 || ^5.0.0-0" + }, + "peerDependenciesMeta": { + "cache-loader": { + "optional": true + }, + "prettier": { + "optional": true + }, + "vue-template-compiler": { + "optional": true + } + } + }, + "node_modules/vue-loader-v16": { + "name": "vue-loader", + "version": "16.8.3", + "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz", + "integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==", + "dev": true, + "optional": true, + "dependencies": { + "chalk": "^4.1.0", + "hash-sum": "^2.0.0", + "loader-utils": "^2.0.0" + }, + "peerDependencies": { + "webpack": "^4.1.0 || ^5.0.0-0" + } + }, + "node_modules/vue-loader-v16/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/vue-loader-v16/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "optional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/vue-loader-v16/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/vue-loader-v16/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "optional": true + }, + "node_modules/vue-loader-v16/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/vue-loader-v16/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "optional": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/vue-loader/node_modules/hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", + "dev": true + }, + "node_modules/vue-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/vue-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/vue-router": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.5.4.tgz", + "integrity": "sha512-x+/DLAJZv2mcQ7glH2oV9ze8uPwcI+H+GgTgTmb5I55bCgY3+vXWIsqbYUzbBSZnwFHEJku4eoaH/x98veyymQ==" + }, + "node_modules/vue-style-loader": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz", + "integrity": "sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==", + "dev": true, + "dependencies": { + "hash-sum": "^1.0.2", + "loader-utils": "^1.0.2" + } + }, + "node_modules/vue-style-loader/node_modules/hash-sum": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", + "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", + "dev": true + }, + "node_modules/vue-style-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/vue-style-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/vue-template-es2015-compiler": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz", + "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", + "dev": true + }, + "node_modules/vue/node_modules/@vue/compiler-sfc": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz", + "integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==", + "dependencies": { + "@babel/parser": "^7.23.5", + "postcss": "^8.4.14", + "source-map": "^0.6.1" + }, + "optionalDependencies": { + "prettier": "^1.18.2 || ^2.0.0" + } + }, + "node_modules/vue/node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/vuetify": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.7.2.tgz", + "integrity": "sha512-qr04ww7uzAPQbpk751x4fSdjsJ+zREzjQ/rBlcQGuWS6MIMFMXcXcwvp4+/tnGsULZxPMWfQ0kmZmg5Yc/XzgQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/johnleider" + }, + "peerDependencies": { + "vue": "^2.6.4" + } + }, + "node_modules/vuetify-loader": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/vuetify-loader/-/vuetify-loader-1.7.3.tgz", + "integrity": "sha512-1Kt6Rfvuw3i9BBlxC9WTMnU3WEU7IBWQmDX+fYGAVGpzWCX7oHythUIwPCZGShHSYcPMKSDbXTPP8UvT5RNw8Q==", + "dev": true, + "dependencies": { + "decache": "^4.6.0", + "file-loader": "^6.2.0", + "loader-utils": "^2.0.0" + }, + "peerDependencies": { + "vue-template-compiler": "^2.6.10", + "vuetify": "^1.3.0 || ^2.0.0", + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/vuetify-loader/node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/vuetify-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/vuex": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz", + "integrity": "sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw==", + "peerDependencies": { + "vue": "^2.0.0" + } + }, + "node_modules/w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "deprecated": "Use your platform's native performance.now() and performance.timeOrigin.", + "dev": true, + "dependencies": { + "browser-process-hrtime": "^1.0.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", + "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", + "dev": true, + "dependencies": { + "domexception": "^1.0.1", + "webidl-conversions": "^4.0.2", + "xml-name-validator": "^3.0.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", + "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" + }, + "optionalDependencies": { + "chokidar": "^3.4.1", + "watchpack-chokidar2": "^2.0.1" + } + }, + "node_modules/watchpack-chokidar2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz", + "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==", + "dev": true, + "optional": true, + "dependencies": { + "chokidar": "^2.1.8" + } + }, + "node_modules/watchpack-chokidar2/node_modules/anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "optional": true, + "dependencies": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "node_modules/watchpack-chokidar2/node_modules/anymatch/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "optional": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "optional": true, + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "optionalDependencies": { + "fsevents": "^1.2.7" + } + }, + "node_modules/watchpack-chokidar2/node_modules/is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", + "dev": true, + "optional": true, + "dependencies": { + "binary-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack-chokidar2/node_modules/readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "node_modules/webpack": { + "version": "4.47.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.47.0.tgz", + "integrity": "sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/wasm-edit": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "acorn": "^6.4.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^4.5.0", + "eslint-scope": "^4.0.3", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.3", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", + "schema-utils": "^1.0.0", + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.3", + "watchpack": "^1.7.4", + "webpack-sources": "^1.4.1" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + }, + "webpack-command": { + "optional": true + } + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.9.0.tgz", + "integrity": "sha512-Ob8amZfCm3rMB1ScjQVlbYYUEJyEjdEtQ92jqiFUYt5VkEeO2v5UMbv49P/gnmCZm3A6yaFQzCBvpZqN4MUsdA==", + "dev": true, + "dependencies": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1", + "bfj": "^6.1.1", + "chalk": "^2.4.1", + "commander": "^2.18.0", + "ejs": "^2.6.1", + "express": "^4.16.3", + "filesize": "^3.6.1", + "gzip-size": "^5.0.0", + "lodash": "^4.17.19", + "mkdirp": "^0.5.1", + "opener": "^1.5.1", + "ws": "^6.0.0" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 6.14.4" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "dev": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/webpack-chain": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/webpack-chain/-/webpack-chain-6.5.1.tgz", + "integrity": "sha512-7doO/SRtLu8q5WM0s7vPKPWX580qhi0/yBHkOxNkv50f6qB76Zy9o2wRTrrPULqYTvQlVHuvbA8v+G5ayuUDsA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "dependencies": { + "deepmerge": "^1.5.2", + "javascript-stringify": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-chain/node_modules/deepmerge": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-1.5.2.tgz", + "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz", + "integrity": "sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==", + "dev": true, + "dependencies": { + "memory-fs": "^0.4.1", + "mime": "^2.4.4", + "mkdirp": "^0.5.1", + "range-parser": "^1.2.1", + "webpack-log": "^2.0.0" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-middleware/node_modules/memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha512-cda4JKCxReDXFXRqOHPQscuIYg1PvxbE2S2GP45rnwfEK+vZaXC8C1OFvdHIbgw0DLzowXGVoxLaAmlgRy14GQ==", + "dev": true, + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "node_modules/webpack-dev-server": { + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.3.tgz", + "integrity": "sha512-3x31rjbEQWKMNzacUZRE6wXvUFuGpH7vr0lIEbYpMAG9BOxi0928QU1BBswOAP3kg3H1O4hiS+sq4YyAn6ANnA==", + "dev": true, + "dependencies": { + "ansi-html-community": "0.0.8", + "bonjour": "^3.5.0", + "chokidar": "^2.1.8", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "debug": "^4.1.1", + "del": "^4.1.1", + "express": "^4.17.1", + "html-entities": "^1.3.1", + "http-proxy-middleware": "0.19.1", + "import-local": "^2.0.0", + "internal-ip": "^4.3.0", + "ip": "^1.1.5", + "is-absolute-url": "^3.0.3", + "killable": "^1.0.1", + "loglevel": "^1.6.8", + "opn": "^5.5.0", + "p-retry": "^3.0.1", + "portfinder": "^1.0.26", + "schema-utils": "^1.0.0", + "selfsigned": "^1.10.8", + "semver": "^6.3.0", + "serve-index": "^1.9.1", + "sockjs": "^0.3.21", + "sockjs-client": "^1.5.0", + "spdy": "^4.0.2", + "strip-ansi": "^3.0.1", + "supports-color": "^6.1.0", + "url": "^0.11.0", + "webpack-dev-middleware": "^3.7.2", + "webpack-log": "^2.0.0", + "ws": "^6.2.1", + "yargs": "^13.3.2" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 6.11.5" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "dependencies": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "node_modules/webpack-dev-server/node_modules/anymatch/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "dev": true, + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "optionalDependencies": { + "fsevents": "^1.2.7" + } + }, + "node_modules/webpack-dev-server/node_modules/cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "dependencies": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "node_modules/webpack-dev-server/node_modules/cliui/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/cliui/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", + "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", + "dev": true, + "dependencies": { + "http-proxy": "^1.17.0", + "is-glob": "^4.0.0", + "lodash": "^4.17.11", + "micromatch": "^3.1.10" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/webpack-dev-server/node_modules/is-absolute-url": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", + "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-dev-server/node_modules/is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", + "dev": true, + "dependencies": { + "binary-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/webpack-dev-server/node_modules/readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/webpack-dev-server/node_modules/string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "dependencies": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/string-width/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/string-width/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-dev-server/node_modules/supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "dev": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/webpack-dev-server/node_modules/yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "dependencies": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "node_modules/webpack-dev-server/node_modules/yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, + "node_modules/webpack-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", + "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "dev": true, + "dependencies": { + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/webpack-log/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/webpack-merge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", + "integrity": "sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "node_modules/webpack/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack/node_modules/enhanced-resolve": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", + "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/webpack/node_modules/enhanced-resolve/node_modules/memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "dev": true, + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + }, + "engines": { + "node": ">=4.3.0 <5.0.0 || >=5.10" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/webpack/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/webpack/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/webpack/node_modules/memory-fs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", + "integrity": "sha512-cda4JKCxReDXFXRqOHPQscuIYg1PvxbE2S2GP45rnwfEK+vZaXC8C1OFvdHIbgw0DLzowXGVoxLaAmlgRy14GQ==", + "dev": true, + "dependencies": { + "errno": "^0.1.3", + "readable-stream": "^2.0.1" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "dependencies": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/webpack/node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "dependencies": { + "iconv-lite": "0.4.24" + } + }, + "node_modules/whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/worker-farm": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "dev": true, + "dependencies": { + "errno": "~0.1.7" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "dependencies": { + "mkdirp": "^0.5.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/write-file-atomic": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.1.tgz", + "integrity": "sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "node_modules/ws": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.4.tgz", + "integrity": "sha512-fFCejsuC8f9kOSu9FYaOw8CdO68O3h5v0lg4p74o8JqWpwTf9tniOD+nOB78aWoVSS6WptVUmDrp/KPsMVBWFQ==", + "dev": true, + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "dev": true + }, + "node_modules/xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/yargs/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/yargs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/yargs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/yargs/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/yargs/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yorkie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yorkie/-/yorkie-2.0.0.tgz", + "integrity": "sha512-jcKpkthap6x63MB4TxwCyuIGkV0oYP/YRyuQU5UO0Yz/E/ZAu+653/uov+phdmO54n6BcvFRyyt0RRrWdN2mpw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "execa": "^0.8.0", + "is-ci": "^1.0.10", + "normalize-path": "^1.0.0", + "strip-indent": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/yorkie/node_modules/ci-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", + "dev": true + }, + "node_modules/yorkie/node_modules/cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", + "dev": true, + "dependencies": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "node_modules/yorkie/node_modules/execa": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.8.0.tgz", + "integrity": "sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA==", + "dev": true, + "dependencies": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/yorkie/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/yorkie/node_modules/is-ci": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "dev": true, + "dependencies": { + "ci-info": "^1.5.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/yorkie/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/yorkie/node_modules/normalize-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-1.0.0.tgz", + "integrity": "sha512-7WyT0w8jhpDStXRq5836AMmihQwq2nrUVQrgjvUo/p/NZf9uy/MeJ246lBJVmWuYXMlJuG9BNZHF0hWjfTbQUA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yorkie/node_modules/strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/yorkie/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true + }, + "node_modules/zwitch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", + "integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..7f57950 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,198 @@ +{ + "name": "eurydice", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "vue-cli-service serve src/main.dev.js", + "prod:origin": "vue-cli-service serve --mode production src/origin/main.js", + "prod:destination": "vue-cli-service serve --mode production src/destination/main.js", + "build:origin": "vue-cli-service build src/origin/main.js --dest dist/origin", + "build:destination": "vue-cli-service build src/destination/main.js --dest dist/destination", + "format": "prettier --write \"**/*.{css,scss,yml,yaml,json,md}\"", + "lint": "vue-cli-service lint --no-fix", + "lint:fix": "vue-cli-service lint --fix", + "lint:css": "stylelint ./src/**/*.{vue,htm,html,css,sss,less,scss,sass}", + "lint:css:fix": "stylelint ./src/**/*.{vue,htm,html,css,sss,less,scss,sass} --fix", + "tests": "jest --clearCache && vue-cli-service test:unit" + }, + "dependencies": { + "@mdi/js": "~7.3.67", + "axios": "~1.7.7", + "bytes": "~3.1.2", + "core-js": "~3.23.3", + "dayjs": "~1.11.10", + "lodash": "~4.17.21", + "typeface-roboto": "~1.1.13", + "vue": "^2.7.0", + "vue-router": "~3.5.3", + "vuetify": "^2.6.15", + "vuex": "~3.6.2" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "~4.5.18", + "@vue/cli-plugin-eslint": "~4.5.18", + "@vue/cli-plugin-router": "~4.5.18", + "@vue/cli-plugin-unit-jest": "~4.5.18", + "@vue/cli-plugin-vuex": "~4.5.18", + "@vue/cli-service": "~4.5.18", + "@vue/eslint-config-airbnb": "~5.3.0", + "@vue/test-utils": "~1.3.0", + "babel-eslint": "~10.1.0", + "babel-plugin-lodash": "~3.3.4", + "eslint": "~6.8.0", + "eslint-config-prettier": "~6.15.0", + "eslint-plugin-import": "~2.25.4", + "eslint-plugin-prettier": "~3.4.1", + "eslint-plugin-vue": "~8.5.0", + "jest-junit": "~13.0.0", + "prettier": "~2.5.1", + "sass": "~1.32.13", + "sass-loader": "~10.2.1", + "stylelint": "~13.13.1", + "stylelint-config-prettier": "~8.0.2", + "stylelint-config-recommended": "~5.0.0", + "stylelint-config-sass-guidelines": "~8.0.0", + "stylelint-config-suitcss": "~17.0.0", + "stylelint-order": "~4.1.0", + "stylelint-prettier": "~1.2.0", + "stylelint-scss": "~3.20.1", + "stylelint-webpack-plugin": "~2.2.2", + "vue-cli-plugin-vuetify": "~2.4.8", + "vue-template-compiler": "^2.7.0", + "vuetify-loader": "~1.7.3" + }, + "eslintConfig": { + "root": true, + "env": { + "node": true + }, + "extends": [ + "eslint:recommended", + "@vue/airbnb", + "plugin:vue/recommended", + "plugin:prettier/recommended", + "prettier/vue" + ], + "parserOptions": { + "parser": "babel-eslint" + }, + "plugins": [ + "prettier" + ], + "rules": { + "import/extensions": [ + "error", + "always", + { + "js": "never", + "vue": "never" + } + ] + }, + "overrides": [ + { + "files": [ + "**/__tests__/*.{j,t}s?(x)", + "**/tests/unit/**/*.{spec,test}.{j,t}s?(x)" + ], + "env": { + "jest": true + } + } + ] + }, + "stylelint": { + "extends": [ + "stylelint-config-recommended", + "stylelint-config-sass-guidelines", + "stylelint-config-suitcss", + "stylelint-prettier/recommended" + ], + "plugin": [ + "stylelint-scss", + "stylelint-order" + ], + "rules": { + "no-invalid-position-at-import-rule": null, + "selector-pseudo-element-no-unknown": [ + true, + { + "ignorePseudoElements": [ + "v-deep" + ] + } + ], + "selector-class-pattern": [ + "^(?:(?:o|c|u|t|s|is|has|_|js|qa)-)?[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*(?:__[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)?(?:--[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)?(?:\\[.+\\])?$", + { + "message": "Class names should match the SUIT CSS naming convention" + } + ], + "selector-max-compound-selectors": 5, + "max-nesting-depth": 5, + "rule-empty-line-before": [ + "always", + { + "except": [ + "after-single-line-comment", + "first-nested" + ] + } + ] + } + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not dead" + ], + "babel": { + "presets": [ + "@vue/cli-plugin-babel/preset" + ], + "plugins": [ + "lodash" + ] + }, + "jest": { + "preset": "@vue/cli-plugin-unit-jest", + "collectCoverage": true, + "coverageDirectory": "/.report", + "reporters": [ + "default", + "jest-junit" + ], + "coverageReporters": [ + "text", + "cobertura", + "lcov" + ], + "collectCoverageFrom": [ + "src/**/*.{js,vue}", + "!src/common/plugins/*.js", + "!src/**/(router|store|constants|settings|main.dev).js" + ], + "testMatch": [ + "/tests/**/?(*.)+(spec|test).[jt]s?(x)" + ], + "moduleNameMapper": { + "axios": "axios/dist/node/axios.cjs", + "^@common/(.*)$": "/src/common/$1", + "^@destination/(.*)$": "/src/destination/$1", + "^@origin/(.*)$": "/src/origin/$1", + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/tests/unit/__mocks__/fileMock.js", + "\\.(css|less)$": "/tests/unit/__mocks__/styleMock.js" + }, + "setupFiles": [ + "/tests/unit/setup.js" + ], + "transform": { + "^.+\\.(js|jsx)$": "babel-jest", + "^.+\\.vue$": "vue-jest" + } + }, + "jest-junit": { + "outputDirectory": "/.report", + "outputName": "junit.xml" + } +} diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 0000000..2e497e5 --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,19 @@ + + + + + + Eurydice + + + + +
+ + + diff --git a/frontend/public/lyre.png b/frontend/public/lyre.png new file mode 100644 index 0000000..e6e8233 Binary files /dev/null and b/frontend/public/lyre.png differ diff --git a/frontend/public/lyre.svg b/frontend/public/lyre.svg new file mode 100644 index 0000000..8c1d947 --- /dev/null +++ b/frontend/public/lyre.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/common/App.vue b/frontend/src/common/App.vue new file mode 100644 index 0000000..a25c155 --- /dev/null +++ b/frontend/src/common/App.vue @@ -0,0 +1,13 @@ + + + + + diff --git a/frontend/src/common/api/serverMetadata.js b/frontend/src/common/api/serverMetadata.js new file mode 100644 index 0000000..c170fbe --- /dev/null +++ b/frontend/src/common/api/serverMetadata.js @@ -0,0 +1,8 @@ +import request from "@common/utils/request"; + +export default function (params = {}) { + return request({ + url: "/metadata/", + params, + }); +} diff --git a/frontend/src/common/api/status.js b/frontend/src/common/api/status.js new file mode 100644 index 0000000..e6ac12a --- /dev/null +++ b/frontend/src/common/api/status.js @@ -0,0 +1,8 @@ +import request from "@common/utils/request"; + +export default function (params = {}) { + return request({ + url: `/status/`, + params, + }); +} diff --git a/frontend/src/common/api/transferables.js b/frontend/src/common/api/transferables.js new file mode 100644 index 0000000..a345f33 --- /dev/null +++ b/frontend/src/common/api/transferables.js @@ -0,0 +1,16 @@ +import request from "@common/utils/request"; + +export function listTransferables(params = {}, abortSignal) { + return request({ + url: "/transferables/", + signal: abortSignal, + params, + }); +} + +export function retrieveTransferable(transferableId, params = {}) { + return request({ + url: `/transferables/${transferableId}/`, + params, + }); +} diff --git a/frontend/src/common/components/AlertsContainer.vue b/frontend/src/common/components/AlertsContainer.vue new file mode 100644 index 0000000..a61b54f --- /dev/null +++ b/frontend/src/common/components/AlertsContainer.vue @@ -0,0 +1,113 @@ + + + + diff --git a/frontend/src/common/components/AppBar/AuthenticatedUser.vue b/frontend/src/common/components/AppBar/AuthenticatedUser.vue new file mode 100644 index 0000000..51dad44 --- /dev/null +++ b/frontend/src/common/components/AppBar/AuthenticatedUser.vue @@ -0,0 +1,24 @@ + + + diff --git a/frontend/src/common/components/AppBar/VersionChip.vue b/frontend/src/common/components/AppBar/VersionChip.vue new file mode 100644 index 0000000..ff4f1cd --- /dev/null +++ b/frontend/src/common/components/AppBar/VersionChip.vue @@ -0,0 +1,15 @@ + + + diff --git a/frontend/src/common/components/AppBar/main.vue b/frontend/src/common/components/AppBar/main.vue new file mode 100644 index 0000000..2e65b90 --- /dev/null +++ b/frontend/src/common/components/AppBar/main.vue @@ -0,0 +1,110 @@ + + + + + diff --git a/frontend/src/common/components/EurydiceFooter.vue b/frontend/src/common/components/EurydiceFooter.vue new file mode 100644 index 0000000..c481e96 --- /dev/null +++ b/frontend/src/common/components/EurydiceFooter.vue @@ -0,0 +1,55 @@ + + + diff --git a/frontend/src/common/components/EurydiceLogo.vue b/frontend/src/common/components/EurydiceLogo.vue new file mode 100644 index 0000000..4c3ea78 --- /dev/null +++ b/frontend/src/common/components/EurydiceLogo.vue @@ -0,0 +1,46 @@ + diff --git a/frontend/src/common/components/StatusBanner.vue b/frontend/src/common/components/StatusBanner.vue new file mode 100644 index 0000000..07f5359 --- /dev/null +++ b/frontend/src/common/components/StatusBanner.vue @@ -0,0 +1,67 @@ + + + diff --git a/frontend/src/common/components/TransferableTable/PaginatorControls.vue b/frontend/src/common/components/TransferableTable/PaginatorControls.vue new file mode 100644 index 0000000..48fc540 --- /dev/null +++ b/frontend/src/common/components/TransferableTable/PaginatorControls.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/frontend/src/common/components/TransferableTable/TransferableStatusChip.vue b/frontend/src/common/components/TransferableTable/TransferableStatusChip.vue new file mode 100644 index 0000000..2389efb --- /dev/null +++ b/frontend/src/common/components/TransferableTable/TransferableStatusChip.vue @@ -0,0 +1,60 @@ + + + diff --git a/frontend/src/common/components/TransferableTable/main.vue b/frontend/src/common/components/TransferableTable/main.vue new file mode 100644 index 0000000..e55a034 --- /dev/null +++ b/frontend/src/common/components/TransferableTable/main.vue @@ -0,0 +1,180 @@ + + + diff --git a/frontend/src/common/layouts/AppBarLayout.vue b/frontend/src/common/layouts/AppBarLayout.vue new file mode 100644 index 0000000..505b4bc --- /dev/null +++ b/frontend/src/common/layouts/AppBarLayout.vue @@ -0,0 +1,33 @@ + + + diff --git a/frontend/src/common/plugins/dayjs.js b/frontend/src/common/plugins/dayjs.js new file mode 100644 index 0000000..cb25048 --- /dev/null +++ b/frontend/src/common/plugins/dayjs.js @@ -0,0 +1,19 @@ +import dayjs from "dayjs"; +import "dayjs/locale/fr"; +import localizedFormat from "dayjs/plugin/localizedFormat"; +import relativeTime from "dayjs/plugin/relativeTime"; +import Vue from "vue"; + +dayjs.locale("fr"); + +dayjs.extend(relativeTime); +dayjs.extend(localizedFormat); + +// https://github.com/Juceztp/vue-dayjs#usage-recommended-without-installing-this-library +Object.defineProperties(Vue.prototype, { + $date: { + get() { + return dayjs; + }, + }, +}); diff --git a/frontend/src/common/plugins/vuealerts.js b/frontend/src/common/plugins/vuealerts.js new file mode 100644 index 0000000..155b706 --- /dev/null +++ b/frontend/src/common/plugins/vuealerts.js @@ -0,0 +1,19 @@ +import AlertsContainer from "@common/components/AlertsContainer"; + +export default { + install(Vue) { + const VueReference = Vue; + + VueReference.prototype.$alert = ( + message, + type = "info", + duration = 5000 + ) => { + AlertsContainer.allInstances.forEach((alertsContainer) => { + alertsContainer.push(message, type, duration); + }); + }; + + VueReference.component("VAlertsContainer", AlertsContainer); + }, +}; diff --git a/frontend/src/common/plugins/vuetify.js b/frontend/src/common/plugins/vuetify.js new file mode 100644 index 0000000..73f51ef --- /dev/null +++ b/frontend/src/common/plugins/vuetify.js @@ -0,0 +1,16 @@ +import EurydiceLogo from "@common/components/EurydiceLogo"; +import Vue from "vue"; +import Vuetify from "vuetify/lib"; + +Vue.use(Vuetify); + +export default new Vuetify({ + icons: { + iconfont: "mdiSvg", + values: { + eurydiceLogo: { + component: EurydiceLogo, + }, + }, + }, +}); diff --git a/frontend/src/common/settings.js b/frontend/src/common/settings.js new file mode 100644 index 0000000..392f109 --- /dev/null +++ b/frontend/src/common/settings.js @@ -0,0 +1,9 @@ +module.exports = { + title: "Eurydice", + baseURL: "/api/v1", + version: process.env.VUE_APP_VERSION || "development", + releaseCycle: "Alpha", + refreshIntervalInMs: 5 * 1000, + serverDownIntervalInMs: 3 * 60 * 1000, // 3 min + transferablesPerPage: 10, +}; diff --git a/frontend/src/common/store.js b/frontend/src/common/store.js new file mode 100644 index 0000000..4a4dd6c --- /dev/null +++ b/frontend/src/common/store.js @@ -0,0 +1,49 @@ +import Vue from "vue"; +import Vuex from "vuex"; + +Vue.use(Vuex); + +export default new Vuex.Store({ + state: () => ({ + username: null, + maintenance: false, + serverDown: false, + serverMetadata: null, + ongoingTransfer: false, + }), + getters: { + maintenance(state) { + return state.maintenance; + }, + serverDown(state) { + return state.serverDown; + }, + serverMetadata(state) { + return state.serverMetadata; + }, + ongoingTransfer(state) { + return state.ongoingTransfer; + }, + }, + mutations: { + setUsername(state, newUsername) { + state.username = newUsername; + }, + setMaintenance(state, newMaintenance) { + state.maintenance = newMaintenance; + }, + setServerDown(state, serverDown) { + state.serverDown = serverDown; + }, + setServerMetadata(state, serverMetadata) { + state.serverMetadata = serverMetadata; + }, + setOngoingTransfer(state, ongoingTransfer) { + state.ongoingTransfer = ongoingTransfer; + }, + }, + actions: {}, + // NOTE: consider adding modules to this common store in main.js + // when adding origin/destination specific state/mutations + modules: {}, +}); diff --git a/frontend/src/common/utils/AutoRefreshMixin.vue b/frontend/src/common/utils/AutoRefreshMixin.vue new file mode 100644 index 0000000..c25f945 --- /dev/null +++ b/frontend/src/common/utils/AutoRefreshMixin.vue @@ -0,0 +1,26 @@ + diff --git a/frontend/src/common/utils/convert-object-keys.js b/frontend/src/common/utils/convert-object-keys.js new file mode 100644 index 0000000..518d672 --- /dev/null +++ b/frontend/src/common/utils/convert-object-keys.js @@ -0,0 +1,26 @@ +import _ from "lodash"; + +function objectKeysTo(obj, caseFunc) { + if (Array.isArray(obj)) { + return obj.map((value) => objectKeysTo(value, caseFunc)); + } + if (_.isPlainObject(obj)) { + return _.reduce( + obj, + (acc, value, key) => { + acc[caseFunc(key)] = objectKeysTo(value, caseFunc); + return acc; + }, + {} + ); + } + return obj; +} + +export function objectKeysToCamelCase(obj) { + return objectKeysTo(obj, _.camelCase); +} + +export function objectKeysToSnakeCase(obj) { + return objectKeysTo(obj, _.snakeCase); +} diff --git a/frontend/src/common/utils/request.js b/frontend/src/common/utils/request.js new file mode 100644 index 0000000..b4c9097 --- /dev/null +++ b/frontend/src/common/utils/request.js @@ -0,0 +1,144 @@ +import { baseURL } from "@common/settings"; +import store from "@common/store"; +import { + objectKeysToCamelCase, + objectKeysToSnakeCase, +} from "@common/utils/convert-object-keys"; +import axios from "axios"; +import _ from "lodash"; +import Vue from "vue"; + +const ERROR_MESSAGES = { + 401: "Authentification nécessaire.", + 404: "Cette ressource n'existe pas.", + 500: "Une erreur serveur est survenue.", +}; + +// create an axios instance +export const service = axios.create({ + baseURL, // url = base url + request url + // withCredentials: true, // send cookies when cross-domain requests + xsrfCookieName: "eurydice_csrftoken", + xsrfHeaderName: "X-CSRFToken", +}); + +/** Convert 'expand' param value from camelCase to snake_case */ +function expandParamValueToSnakeCase(param) { + if (Array.isArray(param)) { + return param.map(_.snakeCase); + } + return _.snakeCase(param); +} + +let moduleDevelopmentAuthenticationInterceptorIndex; +if (process.env.NODE_ENV === "development") { + moduleDevelopmentAuthenticationInterceptorIndex = + service.interceptors.request.use((config) => { + /* eslint-disable no-param-reassign */ + if (config.url === "/user/login/") { + config.headers["X-Remote-User"] = "billmurray"; + } + /* eslint-enable no-param-reassign */ + return config; + }); +} + +export const developmentAuthenticationInterceptorIndex = + moduleDevelopmentAuthenticationInterceptorIndex; + +// request interceptor +export const caseRequestModifierInterceptorIndex = + service.interceptors.request.use((config) => { + /* eslint-disable no-param-reassign */ + // Convert object keys of the parameters from camelCase to snake_case + if (config.params) { + config.params = objectKeysToSnakeCase(config.params); + + if (config.params.expand) { + config.params.expand = expandParamValueToSnakeCase( + config.params.expand + ); + } + } + + // Convert object keys of the payload from camelCase to snake_case + config.data = objectKeysToSnakeCase(config.data); + /* eslint-enable no-param-reassign */ + + return config; + }); + +const setUsernameFromResponse = (response) => { + store.commit("setUsername", response.headers["authenticated-user"] || null); +}; + +// Sets the username in the store from the response headers +export const setUsernameResponseInterceptorIndex = + service.interceptors.response.use( + (response) => { + setUsernameFromResponse(response); + return response; + }, + (error) => { + if (error.response) { + setUsernameFromResponse(error.response); + } + throw error; + } + ); + +const hasBasicAuthHeader = (response) => { + const headerName = "www-authenticate"; + const headerValue = "Basic"; + const headers = response?.headers; + if (headers === undefined) { + return false; + } + return headers[headerName]?.includes(headerValue) === true; +}; + +export function handleResponseError(error) { + // Handle network errors + if (error.response === undefined) { + if (!(error instanceof axios.Cancel)) { + Vue.prototype.$alert(error.message, "error", 10000); + } + } + // handle missing session cookie + else if ( + error.response.status === 401 && + !hasBasicAuthHeader(error.response) + ) { + const LOGIN_ENDPOINT = "/user/login/"; + if (error.response.config?.url === LOGIN_ENDPOINT) { + Vue.prototype.$alert("Authentication error", "error", 10000); + } else { + return service + .request(LOGIN_ENDPOINT) + .then(() => service.request(error.config)) + .catch((authenticationError) => + Vue.prototype.$alert(authenticationError.message, "error", 10000) + ); + } + } + // handle common HTTP errors + else if (error.response.status in ERROR_MESSAGES) { + Vue.prototype.$alert(ERROR_MESSAGES[error.response.status], "error", 10000); + } + throw error; +} + +// response interceptor +export const responseInterceptorIndex = service.interceptors.response.use( + /** + * If you want to get http information such as headers or status + * Please return response => response + */ + (response) => { + // Convert object keys of the payload from snake_case to camelCase + return objectKeysToCamelCase(response.data); + }, + handleResponseError +); + +export default service; diff --git a/frontend/src/common/views/404View.vue b/frontend/src/common/views/404View.vue new file mode 100644 index 0000000..0c73f88 --- /dev/null +++ b/frontend/src/common/views/404View.vue @@ -0,0 +1,26 @@ + + + diff --git a/frontend/src/destination/api/association.js b/frontend/src/destination/api/association.js new file mode 100644 index 0000000..900c343 --- /dev/null +++ b/frontend/src/destination/api/association.js @@ -0,0 +1,10 @@ +import request from "@common/utils/request"; + +// eslint-disable-next-line import/prefer-default-export +export function postAssociationToken(token) { + return request({ + url: `/user/association/`, + method: "POST", + data: { token }, + }); +} diff --git a/frontend/src/destination/api/transferables.js b/frontend/src/destination/api/transferables.js new file mode 100644 index 0000000..556f7f6 --- /dev/null +++ b/frontend/src/destination/api/transferables.js @@ -0,0 +1,9 @@ +import request from "@common/utils/request"; + +// eslint-disable-next-line import/prefer-default-export +export function deleteTransferable(transferableId) { + return request({ + url: `/transferables/${transferableId}/`, + method: "DELETE", + }); +} diff --git a/frontend/src/destination/components/DestinationTransferableTable/TransferableActions.vue b/frontend/src/destination/components/DestinationTransferableTable/TransferableActions.vue new file mode 100644 index 0000000..b74ddb8 --- /dev/null +++ b/frontend/src/destination/components/DestinationTransferableTable/TransferableActions.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/frontend/src/destination/components/DestinationTransferableTable/TransferableDeleteButton.vue b/frontend/src/destination/components/DestinationTransferableTable/TransferableDeleteButton.vue new file mode 100644 index 0000000..8e96c9f --- /dev/null +++ b/frontend/src/destination/components/DestinationTransferableTable/TransferableDeleteButton.vue @@ -0,0 +1,61 @@ + + + diff --git a/frontend/src/destination/components/DestinationTransferableTable/TransferableDownloadButton.vue b/frontend/src/destination/components/DestinationTransferableTable/TransferableDownloadButton.vue new file mode 100644 index 0000000..12ecadc --- /dev/null +++ b/frontend/src/destination/components/DestinationTransferableTable/TransferableDownloadButton.vue @@ -0,0 +1,24 @@ + + + diff --git a/frontend/src/destination/components/DestinationTransferableTable/TransferablePreviewButton.vue b/frontend/src/destination/components/DestinationTransferableTable/TransferablePreviewButton.vue new file mode 100644 index 0000000..de91fe2 --- /dev/null +++ b/frontend/src/destination/components/DestinationTransferableTable/TransferablePreviewButton.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/frontend/src/destination/components/DestinationTransferableTable/main.vue b/frontend/src/destination/components/DestinationTransferableTable/main.vue new file mode 100644 index 0000000..8767b58 --- /dev/null +++ b/frontend/src/destination/components/DestinationTransferableTable/main.vue @@ -0,0 +1,126 @@ + + + diff --git a/frontend/src/destination/constants.js b/frontend/src/destination/constants.js new file mode 100644 index 0000000..39d9892 --- /dev/null +++ b/frontend/src/destination/constants.js @@ -0,0 +1,10 @@ +/* eslint-disable import/prefer-default-export */ + +export const TRANSFERABLE_STATES = { + SUCCESS: "SUCCESS", + ONGOING: "ONGOING", + ERROR: "ERROR", + REVOKED: "REVOKED", + EXPIRED: "EXPIRED", + REMOVED: "REMOVED", +}; diff --git a/frontend/src/destination/main.js b/frontend/src/destination/main.js new file mode 100644 index 0000000..4d63070 --- /dev/null +++ b/frontend/src/destination/main.js @@ -0,0 +1,22 @@ +import App from "@common/App"; +import "@common/plugins/dayjs"; +import VueAlerts from "@common/plugins/vuealerts"; +import vuetify from "@common/plugins/vuetify"; +import store from "@common/store"; +import intercetorSetup from "@destination/utils/interceptors"; +import Vue from "vue"; +import router from "./router"; + +Vue.config.productionTip = false; + +Vue.use(VueAlerts); + +new Vue({ + router, + store, + mounted() { + intercetorSetup(); + }, + vuetify, + render: (h) => h(App), +}).$mount("#app"); diff --git a/frontend/src/destination/router.js b/frontend/src/destination/router.js new file mode 100644 index 0000000..6115bb4 --- /dev/null +++ b/frontend/src/destination/router.js @@ -0,0 +1,36 @@ +import Vue from "vue"; +import VueRouter from "vue-router"; + +Vue.use(VueRouter); + +const routes = [ + { + path: "/", + component: () => import("@common/layouts/AppBarLayout"), + children: [ + { + path: "/", + name: "home", + component: () => import("@destination/views/DestinationHome"), + }, + { + path: "/user-association", + name: "userAssociation", + component: () => import("@destination/views/UserAssociation"), + }, + ], + }, + { + path: "/404", + component: () => import("@common/views/404View"), + }, + { path: "*", redirect: "/404" }, +]; + +const router = new VueRouter({ + mode: "history", + base: process.env.BASE_URL, + routes, +}); + +export default router; diff --git a/frontend/src/destination/settings.js b/frontend/src/destination/settings.js new file mode 100644 index 0000000..a8e639e --- /dev/null +++ b/frontend/src/destination/settings.js @@ -0,0 +1,3 @@ +module.exports = { + associationTokenWordCount: 18, +}; diff --git a/frontend/src/destination/utils/interceptors.js b/frontend/src/destination/utils/interceptors.js new file mode 100644 index 0000000..f8e961e --- /dev/null +++ b/frontend/src/destination/utils/interceptors.js @@ -0,0 +1,17 @@ +import axiosClient from "@common/utils/request"; +import router from "@destination/router"; + +const UNASSOCIATED_USER_HTTP_STATUS_CODE = 403; + +export default function setup() { + axiosClient.interceptors.response.use( + (response) => response, + // eslint-disable-next-line require-await + async (error) => { + if (error.response.status === UNASSOCIATED_USER_HTTP_STATUS_CODE) { + router.push({ name: "userAssociation" }); + } + throw error; + } + ); +} diff --git a/frontend/src/destination/views/DestinationHome.vue b/frontend/src/destination/views/DestinationHome.vue new file mode 100644 index 0000000..02a262d --- /dev/null +++ b/frontend/src/destination/views/DestinationHome.vue @@ -0,0 +1,20 @@ + + + diff --git a/frontend/src/destination/views/UserAssociation.vue b/frontend/src/destination/views/UserAssociation.vue new file mode 100644 index 0000000..80a9eef --- /dev/null +++ b/frontend/src/destination/views/UserAssociation.vue @@ -0,0 +1,133 @@ + + + + diff --git a/frontend/src/main.dev.js b/frontend/src/main.dev.js new file mode 100644 index 0000000..e2ea311 --- /dev/null +++ b/frontend/src/main.dev.js @@ -0,0 +1,26 @@ +import App from "@common/App"; +import "@common/plugins/dayjs"; +import VueAlerts from "@common/plugins/vuealerts"; +import vuetify from "@common/plugins/vuetify"; +import store from "@common/store"; +import destinationRouter from "@destination/router"; +import destinationInterceptorSetup from "@destination/utils/interceptors"; +import originRouter from "@origin/router"; +import Vue from "vue"; + +const subdomain = window.location.host.split(".")[0]; +const subdomainIsOrigin = subdomain === "origin"; + +Vue.use(VueAlerts); + +new Vue({ + router: subdomainIsOrigin ? originRouter : destinationRouter, + store, + mounted() { + if (!subdomainIsOrigin) { + destinationInterceptorSetup(); + } + }, + vuetify, + render: (h) => h(App), +}).$mount("#app"); diff --git a/frontend/src/origin/api/association.js b/frontend/src/origin/api/association.js new file mode 100644 index 0000000..8c32cb0 --- /dev/null +++ b/frontend/src/origin/api/association.js @@ -0,0 +1,9 @@ +import request from "@common/utils/request"; + +// eslint-disable-next-line import/prefer-default-export +export function getAssociationToken(params = {}) { + return request({ + url: `/user/association/`, + params, + }); +} diff --git a/frontend/src/origin/api/transferables.js b/frontend/src/origin/api/transferables.js new file mode 100644 index 0000000..efca3e6 --- /dev/null +++ b/frontend/src/origin/api/transferables.js @@ -0,0 +1,37 @@ +import request from "@common/utils/request"; +import { TRANSFERABLE_MAX_SIZE } from "@origin/constants"; + +export function cancelTransferable(transferableId) { + return request({ + url: `/transferables/${transferableId}/`, + method: "DELETE", + }); +} + +export function createTransferable(file) { + return request({ + url: "/transferables/", + method: "POST", + data: file, + headers: { + "Content-Type": "application/octet-stream", + "Metadata-Name": encodeURI(file.name), + }, + onUploadProgress: file.progressUpdater, + signal: file.abortController.signal, + timeout: 0, + }); +} + +export function validateTransferable(file) { + if (file?.size > TRANSFERABLE_MAX_SIZE) { + const err = new Error("413 Content Too Large"); + err.response = { + status: 413, + data: { + detail: `La taille du fichier dépasse le maximum autorisé (${TRANSFERABLE_MAX_SIZE})`, + }, + }; + throw err; + } +} diff --git a/frontend/src/origin/components/DropZone.vue b/frontend/src/origin/components/DropZone.vue new file mode 100644 index 0000000..28561d7 --- /dev/null +++ b/frontend/src/origin/components/DropZone.vue @@ -0,0 +1,256 @@ + + + + + diff --git a/frontend/src/origin/components/DropZoneFile.vue b/frontend/src/origin/components/DropZoneFile.vue new file mode 100644 index 0000000..29f37de --- /dev/null +++ b/frontend/src/origin/components/DropZoneFile.vue @@ -0,0 +1,36 @@ + + + + + diff --git a/frontend/src/origin/components/OriginTransferableTable/TransferableCancelButton.vue b/frontend/src/origin/components/OriginTransferableTable/TransferableCancelButton.vue new file mode 100644 index 0000000..2995ac3 --- /dev/null +++ b/frontend/src/origin/components/OriginTransferableTable/TransferableCancelButton.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/frontend/src/origin/components/OriginTransferableTable/main.vue b/frontend/src/origin/components/OriginTransferableTable/main.vue new file mode 100644 index 0000000..57baa9f --- /dev/null +++ b/frontend/src/origin/components/OriginTransferableTable/main.vue @@ -0,0 +1,103 @@ + + + diff --git a/frontend/src/origin/components/TextTransferZone.vue b/frontend/src/origin/components/TextTransferZone.vue new file mode 100644 index 0000000..b8f1ed3 --- /dev/null +++ b/frontend/src/origin/components/TextTransferZone.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/frontend/src/origin/components/UserAssociation.vue b/frontend/src/origin/components/UserAssociation.vue new file mode 100644 index 0000000..9b621af --- /dev/null +++ b/frontend/src/origin/components/UserAssociation.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/frontend/src/origin/constants.js b/frontend/src/origin/constants.js new file mode 100644 index 0000000..6a8e53d --- /dev/null +++ b/frontend/src/origin/constants.js @@ -0,0 +1,13 @@ +/* eslint-disable import/prefer-default-export */ + +export const TRANSFERABLE_STATES = { + PENDING: "PENDING", + SUCCESS: "SUCCESS", + ONGOING: "ONGOING", + ERROR: "ERROR", + CANCELED: "CANCELED", +}; + +// Maximum object size per operation allowed by minio (50 TiB) +// https://min.io/docs/minio/linux/operations/checklists/thresholds.html#minio-server-limits +export const TRANSFERABLE_MAX_SIZE = 54975581388800; diff --git a/frontend/src/origin/main.js b/frontend/src/origin/main.js new file mode 100644 index 0000000..71555ca --- /dev/null +++ b/frontend/src/origin/main.js @@ -0,0 +1,18 @@ +import App from "@common/App.vue"; +import "@common/plugins/dayjs"; +import VueAlerts from "@common/plugins/vuealerts"; +import vuetify from "@common/plugins/vuetify"; +import store from "@common/store"; +import Vue from "vue"; +import router from "./router"; + +Vue.config.productionTip = false; + +Vue.use(VueAlerts); + +new Vue({ + router, + store, + vuetify, + render: (h) => h(App), +}).$mount("#app"); diff --git a/frontend/src/origin/router.js b/frontend/src/origin/router.js new file mode 100644 index 0000000..01787b3 --- /dev/null +++ b/frontend/src/origin/router.js @@ -0,0 +1,35 @@ +import Vue from "vue"; +import VueRouter from "vue-router"; +import UserAssociation from "@origin/components/UserAssociation"; + +Vue.use(VueRouter); + +const routes = [ + { + path: "/", + component: () => import("@common/layouts/AppBarLayout"), + children: [ + { + path: "/", + name: "home", + component: () => import("@origin/views/OriginHome"), + }, + ], + props: { + extraComponent: UserAssociation, + }, + }, + { + path: "/404", + component: () => import("@common/views/404View"), + }, + { path: "*", redirect: "/404" }, +]; + +const router = new VueRouter({ + mode: "history", + base: process.env.BASE_URL, + routes, +}); + +export default router; diff --git a/frontend/src/origin/views/OriginHome.vue b/frontend/src/origin/views/OriginHome.vue new file mode 100644 index 0000000..bd93b2f --- /dev/null +++ b/frontend/src/origin/views/OriginHome.vue @@ -0,0 +1,23 @@ + + + diff --git a/frontend/tests/unit/__mocks__/fileMock.js b/frontend/tests/unit/__mocks__/fileMock.js new file mode 100644 index 0000000..e69de29 diff --git a/frontend/tests/unit/__mocks__/styleMock.js b/frontend/tests/unit/__mocks__/styleMock.js new file mode 100644 index 0000000..f053ebf --- /dev/null +++ b/frontend/tests/unit/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/frontend/tests/unit/common/components/AppBar/AuthenticatedUser.test.js b/frontend/tests/unit/common/components/AppBar/AuthenticatedUser.test.js new file mode 100644 index 0000000..75c842e --- /dev/null +++ b/frontend/tests/unit/common/components/AppBar/AuthenticatedUser.test.js @@ -0,0 +1,31 @@ +import AuthenticatedUser from "@common/components/AppBar/AuthenticatedUser"; +import { createLocalVue, mount } from "@vue/test-utils"; +import Vuetify from "vuetify"; +import Vuex from "vuex"; + +describe("AuthenticatedUser", () => { + let localVue; + let vuetify; + let store; + + const username = "billmurray"; + + beforeEach(() => { + vuetify = new Vuetify(); + localVue = createLocalVue(); + localVue.use(Vuex); + store = new Vuex.Store({ + state: () => ({ username }), + }); + }); + + it("displays username from vuex store", async () => { + // mock child components + const wrapper = mount(AuthenticatedUser, { + localVue, + vuetify, + store, + }); + expect(wrapper.find(".text-body-1").text()).toBe(username); + }); +}); diff --git a/frontend/tests/unit/common/components/AppBar/VersionChip.test.js b/frontend/tests/unit/common/components/AppBar/VersionChip.test.js new file mode 100644 index 0000000..1b1064b --- /dev/null +++ b/frontend/tests/unit/common/components/AppBar/VersionChip.test.js @@ -0,0 +1,17 @@ +import VersionChip from "@common/components/AppBar/VersionChip"; +import { shallowMount } from "@vue/test-utils"; + +jest.mock("@common/settings", () => ({ + version: "Tony", + releaseCycle: "Stark", +})); + +describe("VersionChip", () => { + it("displays correct version", async () => { + const wrapper = shallowMount(VersionChip); + const version = wrapper.find("code"); + + expect(version.isVisible()).toBe(true); + expect(version.text()).toBe("Stark Tony"); + }); +}); diff --git a/frontend/tests/unit/common/components/AppBar/main.test.js b/frontend/tests/unit/common/components/AppBar/main.test.js new file mode 100644 index 0000000..fc22a57 --- /dev/null +++ b/frontend/tests/unit/common/components/AppBar/main.test.js @@ -0,0 +1,19 @@ +import AppBar from "@common/components/AppBar/main"; +import { shallowMount } from "@vue/test-utils"; +import store from "@common/store"; + +describe("AppBar", () => { + const UserAssociationStub = { + render(createElement) { + return createElement("div", "UserAssociationStub"); + }, + }; + + it("renders child components", async () => { + const wrapper = shallowMount(AppBar, { + store, + propsData: { extraComponent: UserAssociationStub }, + }); + expect(wrapper.getComponent(UserAssociationStub).isVisible()).toBe(true); + }); +}); diff --git a/frontend/tests/unit/common/components/Footer.test.js b/frontend/tests/unit/common/components/Footer.test.js new file mode 100644 index 0000000..7595557 --- /dev/null +++ b/frontend/tests/unit/common/components/Footer.test.js @@ -0,0 +1,20 @@ +import EurydiceFooter from "@common/components/EurydiceFooter"; +import { mount } from "@vue/test-utils"; +import store from "@common/store"; + +describe("Mounted EurydiceFooter", () => { + const wrapper = mount(EurydiceFooter, { store }); + + it("renders correctly", async () => { + await wrapper.setData({ + links: [ + { label: "foo", url: "bar" }, + { + label: "i like", + url: "waffles", + }, + ], + }); + expect(wrapper.element).toMatchSnapshot(); + }); +}); diff --git a/frontend/tests/unit/common/components/TransferableTable/PaginatorControls.test.js b/frontend/tests/unit/common/components/TransferableTable/PaginatorControls.test.js new file mode 100644 index 0000000..bf87f7d --- /dev/null +++ b/frontend/tests/unit/common/components/TransferableTable/PaginatorControls.test.js @@ -0,0 +1,50 @@ +import PaginatorControls from "@common/components/TransferableTable/PaginatorControls"; +import { createLocalVue, mount, shallowMount } from "@vue/test-utils"; +import Vuetify from "vuetify"; + +describe("PaginatorControls", () => { + it("renders correctly", async () => { + const localVue = createLocalVue(); + const vuetify = new Vuetify(); + const wrapper = mount(PaginatorControls, { + localVue, + vuetify, + propsData: { + numPages: 6, + }, + }); + expect(wrapper.element).toMatchSnapshot(); + }); + + it.each` + requestedPage | result + ${Number.MIN_SAFE_INTEGER.toString()} | ${1} + ${"-1"} | ${1} + ${"0"} | ${1} + ${"1"} | ${1} + ${"5"} | ${5} + ${"10"} | ${10} + ${"11"} | ${1} + ${"a"} | ${1} + ${""} | ${1} + ${Number.MAX_SAFE_INTEGER.toString()} | ${1} + `( + `goToPage should set the current page to $result when the page requested is $requestedPage`, + ({ requestedPage, result }) => { + const wrapper = shallowMount(PaginatorControls, { + attrs: { + page: 1, + numPages: 10, + }, + }); + + const event = { + target: { value: requestedPage }, + }; + + wrapper.vm.goToPage(event); + + expect(wrapper.vm.page).toBe(result); + } + ); +}); diff --git a/frontend/tests/unit/common/components/TransferableTable/TransferableStatusChip.test.js b/frontend/tests/unit/common/components/TransferableTable/TransferableStatusChip.test.js new file mode 100644 index 0000000..306b9b3 --- /dev/null +++ b/frontend/tests/unit/common/components/TransferableTable/TransferableStatusChip.test.js @@ -0,0 +1,34 @@ +import TransferableStatusChip from "@common/components/TransferableTable/TransferableStatusChip"; +import { createLocalVue, mount } from "@vue/test-utils"; +import Vuetify from "vuetify"; +import store from "@/common/store"; + +describe("TransferableStatusChip", () => { + const localVue = createLocalVue(); + const vuetify = new Vuetify(); + + const tooltipText = "The world is a sphere. There is no East or West."; + const wrapper = mount(TransferableStatusChip, { + localVue, + vuetify, + store, + propsData: { + state: "SUCCESS", + stateLabel: "Succès", + icon: "mdi-check", + color: "success", + tooltipText, + }, + }); + it("renders correctly", async () => { + expect(wrapper.element).toMatchSnapshot(); + }); + it("displays given tooltip text", async () => { + wrapper.findComponent(TransferableStatusChip).trigger("mouseenter"); + await wrapper.vm.$nextTick(); + + requestAnimationFrame(() => { + expect(wrapper.find("span").text()).toBe(tooltipText); + }); + }); +}); diff --git a/frontend/tests/unit/common/components/TransferableTable/TransferablesTable.test.js b/frontend/tests/unit/common/components/TransferableTable/TransferablesTable.test.js new file mode 100644 index 0000000..81f55f8 --- /dev/null +++ b/frontend/tests/unit/common/components/TransferableTable/TransferablesTable.test.js @@ -0,0 +1,194 @@ +import { listTransferables } from "@common/api/transferables"; +import TransferablesTable from "@common/components/TransferableTable/main"; +import { transferablesPerPage } from "@common/settings"; +import { createLocalVue, mount, shallowMount } from "@vue/test-utils"; +import Vuetify from "vuetify"; +import store from "@/common/store"; + +jest.mock("@common/api/transferables", () => ({ + listTransferables: jest.fn(), +})); + +// hardcode transferablesPerPage +// NOTE: we cannot use a variable here because jest.mock calls are executed before imports +// https://jestjs.io/docs/manual-mocks#using-with-es-module-imports +jest.mock("@common/settings", () => ({ + transferablesPerPage: 10, +})); + +describe("TransferablesTable", () => { + // vuetify instance is not used in shallowMount + const localVue = createLocalVue(); + let vuetify; + + beforeEach(() => { + vuetify = new Vuetify(); + }); + + const PaginatorControlsStub = { + template: "
PaginatorControlsStub
", + data() { + return { + page: 1, + }; + }, + }; + + const dummyProps = { + headers: [ + { text: "Sith", value: "isJedi" }, + { text: "Jedi", value: "isSith" }, + ], + states: { + ALIVE: { + color: "success lighten-1", + label: "Vivant", + icon: "mdi-check", + }, + DEAD: { + color: "error lighten-1", + label: "Mort", + icon: "mdi-alert-circle", + }, + }, + }; + + // using jest.spyOn to override created lifecycle method because vue-test-utils + // has no build in tools for that: + // https://github.com/vuejs/vue-test-utils/issues/166 + jest.spyOn(TransferablesTable, "created").mockImplementation(jest.fn()); + + it("renders correctly", async () => { + const wrapper = mount(TransferablesTable, { + localVue, + vuetify, + stubs: { PaginatorControls: PaginatorControlsStub }, + propsData: dummyProps, + }); + expect(wrapper.element).toMatchSnapshot(); + }); + + it.each` + transferableCount | expectedNumPages + ${0} | ${0} + ${1} | ${1} + ${10} | ${1} + ${11} | ${2} + ${20} | ${2} + `( + `updates page count to $expectedNumPages with $transferableCount transferables`, + async ({ transferableCount, expectedNumPages }) => { + const wrapper = shallowMount(TransferablesTable, { + propsData: dummyProps, + }); + expect(wrapper.vm.numPages).toBe(0); + + const newTransferables = { count: transferableCount }; + + await wrapper.setData({ transferables: newTransferables }); + + expect(wrapper.vm.transferables.count).toBe(transferableCount); + + expect(wrapper.vm.numPages).toBe(expectedNumPages); + } + ); + + it.each([true, false])( + "getTransferableFrom queries the given URL when initialShowRefresh is %p", + async (initialShowRefresh) => { + const wrapper = shallowMount(TransferablesTable, { + store, + data() { + return { + transferables: { + pages: { next: "supertoken" }, + showRefresh: initialShowRefresh, + }, + }; + }, + propsData: dummyProps, + }); + + listTransferables.mockReturnValue({ newItems: true }); + + await wrapper.vm.getTransferableFrom("next", false, false); + + expect(listTransferables).toHaveBeenCalledWith( + { + page: "supertoken", + pageSize: transferablesPerPage, + }, + expect.any(AbortSignal) + ); + listTransferables.mockClear(); + + expect(wrapper.vm.isLoading).toBe(false); + expect(wrapper.vm.showRefresh).toBe(true); + } + ); + it("getTransferablesPage correctly queries the given page", async () => { + const wrapper = mount(TransferablesTable, { + localVue, + vuetify, + stubs: { PaginatorControls: PaginatorControlsStub }, + propsData: dummyProps, + data() { + return { + transferables: { + offset: transferablesPerPage * 4, + pages: { current: "supertoken" }, + }, + }; + }, + }); + + const expectedTransferables = { meAnd: "michael" }; + + listTransferables.mockReturnValue(expectedTransferables); + + const requestedPage = 2; + + await wrapper.vm.getTransferablesPage(requestedPage); + + expect(listTransferables).toHaveBeenCalledWith({ + delta: -3, + from: "supertoken", + pageSize: transferablesPerPage, + }); + listTransferables.mockClear(); + + expect(wrapper.vm.isLoading).toBe(false); + expect(wrapper.vm.transferables).toEqual(expectedTransferables); + }); + it("getFreshFirstPage correctly queries the first page", async () => { + const wrapper = mount(TransferablesTable, { + localVue, + vuetify, + stubs: { PaginatorControls: PaginatorControlsStub }, + propsData: dummyProps, + data() { + return { + showRefresh: true, + }; + }, + }); + + const expectedTransferables = { meAnd: "michael" }; + + listTransferables.mockReturnValue(expectedTransferables); + + await wrapper.vm.getFreshFirstPage(); + + expect(listTransferables).toHaveBeenCalledWith({ + pageSize: transferablesPerPage, + }); + listTransferables.mockClear(); + + expect(wrapper.vm.showRefresh).toBe(false); + expect(wrapper.vm.isLoading).toBe(false); + expect(wrapper.vm.transferables).toEqual(expectedTransferables); + // If the page change does not come from the PaginatorControls component, + // the TransferableTable component must change the page number manually + expect(wrapper.vm.$refs.paginatorControls.page).toEqual(1); + }); +}); diff --git a/frontend/tests/unit/common/components/TransferableTable/__snapshots__/PaginatorControls.test.js.snap b/frontend/tests/unit/common/components/TransferableTable/__snapshots__/PaginatorControls.test.js.snap new file mode 100644 index 0000000..cc41f1e --- /dev/null +++ b/frontend/tests/unit/common/components/TransferableTable/__snapshots__/PaginatorControls.test.js.snap @@ -0,0 +1,120 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PaginatorControls renders correctly 1`] = ` +
+ + +
+
+
+ +
+ + +
+ sur 6 +
+
+
+
+
+ +
+
+
+
+ + +
+`; diff --git a/frontend/tests/unit/common/components/TransferableTable/__snapshots__/TransferableStatusChip.test.js.snap b/frontend/tests/unit/common/components/TransferableTable/__snapshots__/TransferableStatusChip.test.js.snap new file mode 100644 index 0000000..d5fd5d5 --- /dev/null +++ b/frontend/tests/unit/common/components/TransferableTable/__snapshots__/TransferableStatusChip.test.js.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TransferableStatusChip renders correctly 1`] = ` + + + + +